Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"license": "Apache-2.0",
"dependencies": {
"node-gyp-build": "^4.8.4",
"pprof-format": "^2.2.1",
"pprof-format": "^2.3.0",
"source-map": "^0.8.0"
},
"devDependencies": {
Expand Down
39 changes: 27 additions & 12 deletions ts/src/profile-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,18 @@ function isGeneratedLocation(
* (`column << 32 | line`) — the encoding the Datadog deobfuscation backend
* decodes (the same one the Chrome profile intake uses). Required to
* deobfuscate single-line (bundled/minified) frames, where the column is the
* only discriminator between functions.
*
* The enum leaves room for a future `'emit'` mode that would populate a real
* pprof `Line.column` field once the backend consumes it.
* only discriminator between functions. Only frames whose source map was
* declared but missing locally are packed.
* - `'emit'`: like `'pack'`, but records the column in the dedicated pprof
* `Line.column` field instead of packing it into the line field, so the line
* field stays standards-compliant and the two concerns are cleanly separated.
* Scoped to the same frames as `'pack'` — only those whose source map was
* declared but missing locally, i.e. the frames bound for server-side
* unminification. The Datadog backend performs the `column << 32 | line`
* packing itself (for Node.js profiles) when it consumes the column field.
* Requires pprof-format >= 2.3.0, the version that added `Line.column`.
*/
export type ColumnNumbers = 'drop' | 'pack';
export type ColumnNumbers = 'drop' | 'pack' | 'emit';

export const DEFAULT_COLUMN_NUMBERS: ColumnNumbers = 'drop';

Expand Down Expand Up @@ -158,6 +164,7 @@ function serialize<T extends ProfileNode>(
const functionIdMap = new Map<string, number>();
const locationIdMap = new Map<string, number>();
const packColumns = columnNumbers === 'pack';
const emitColumns = columnNumbers === 'emit';

let hasMissingMapFiles = false;

Expand Down Expand Up @@ -230,15 +237,23 @@ function serialize<T extends ProfileNode>(
}

function getLine(loc: SourceLocation, scriptId?: number): Line {
// Only pack the column for frames whose source map was declared but missing
// locally — i.e. exactly the frames that will be sent for server-side
// unminification (the same condition that sets dd:has-missing-map-files).
// Locally-resolved frames keep their plain line, so packed values never
// reach profiles that skip server-side unminification.
const packColumn = packColumns && loc.missingMapFile === true;
// Both 'pack' and 'emit' carry the column only for frames whose source map
// was declared but missing locally — i.e. exactly the frames bound for
// server-side unminification (the same condition that sets
// dd:has-missing-map-files). Locally-resolved frames keep a plain line and
// no column, so column data never reaches profiles that skip server-side
// unminification.
const carryColumn = loc.missingMapFile === true;
return new Line({
functionId: getFunction(loc, scriptId).id,
line: packColumn ? packLineAndColumn(loc.line, loc.column) : loc.line,
// 'pack' encodes the column into the high 32 bits of the line field.
line:
packColumns && carryColumn
? packLineAndColumn(loc.line, loc.column)
: loc.line,
// 'emit' records the column in the dedicated pprof Line.column field
// instead; the backend does the line/column packing itself.
column: emitColumns && carryColumn ? loc.column : undefined,
});
}

Expand Down
5 changes: 3 additions & 2 deletions ts/src/time-profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ export interface TimeProfilerOptions {
* Controls how frame column numbers are represented in the serialized
* profile. Defaults to `'drop'` (column omitted) to preserve the historical
* line-number semantics for existing consumers. Set to `'pack'` to pack the
* column into the high 32 bits of the line field for backends that support
* it (e.g. Datadog's JS/Node deobfuscation). See {@link ColumnNumbers}.
* column into the high 32 bits of the line field, or `'emit'` to populate the
* dedicated pprof `Line.column` field, for backends that support it (e.g.
* Datadog's JS/Node deobfuscation). See {@link ColumnNumbers}.
*/
columnNumbers?: ColumnNumbers;
}
Expand Down
42 changes: 42 additions & 0 deletions ts/test/test-profile-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,48 @@ describe('profile-serializer', () => {
);
});

it('emits the real column in the pprof Line.column field and keeps the line plain when columnNumbers is "emit"', () => {
// 'emit' never packs: the line field stays plain and the generated column
// (1) is written to the dedicated pprof Line.column field. The backend
// performs the line/column packing itself for Node.js profiles.
const profile = serializeTimeProfile(
makeSingleNodeTimeProfile(missingJsPath),
1000,
sourceMapper,
false,
undefined,
[],
'emit',
);
assertHasMissingMapToken(profile);
const line = profile.location![0].line![0];
assert.strictEqual(BigInt(line.line), 1n);
assert.strictEqual(BigInt(line.column), 1n);
});

it('does not carry a column under "emit" for a frame with no missing map', () => {
// 'emit' is scoped to the same frames as 'pack': only those whose map was
// declared but missing locally. With no source mapper the frame is neither
// resolved nor flagged missing, so no column is emitted (Line.column
// defaults to 0) and the line stays plain.
const profile = serializeTimeProfile(
makeSingleNodeTimeProfile(missingJsPath),
1000,
undefined,
false,
undefined,
[],
'emit',
);
assert.ok(
!profile.comment || profile.comment.length === 0,
'expected no missing-map token without a source mapper',
);
const line = profile.location![0].line![0];
assert.strictEqual(BigInt(line.line), 1n);
assert.strictEqual(BigInt(line.column), 0n);
});

it('leaves a missing-map frame line plain under the default "drop"', () => {
const profile = serializeTimeProfile(
makeSingleNodeTimeProfile(missingJsPath),
Expand Down
Loading