Skip to content
Open
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
9 changes: 9 additions & 0 deletions ts/src/profile-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ function serialize<T extends ProfileNode>(
const functionIdMap = new Map<string, number>();
const locationIdMap = new Map<string, number>();

let hasMissingMapFiles = false;

const entries: Array<Entry<T>> = (root.children as T[]).map((n: T) => ({
node: n,
stack: [],
Expand Down Expand Up @@ -131,6 +133,10 @@ function serialize<T extends ProfileNode>(
profile.function = functions;
profile.stringTable = stringTable;

if (hasMissingMapFiles) {
profile.comment = [stringTable.dedup('dd:has-missing-map-files')];
}

function getLocation(
node: ProfileNode,
scriptName: string,
Expand All @@ -146,6 +152,9 @@ function serialize<T extends ProfileNode>(
if (profLoc.line) {
if (sourceMapper && isGeneratedLocation(profLoc)) {
profLoc = sourceMapper.mappingInfo(profLoc);
if (profLoc.missingMapFile) {
hasMissingMapFiles = true;
}
}
}
const keyStr = `${node.scriptId}:${profLoc.line}:${profLoc.column}:${profLoc.name}`;
Expand Down
19 changes: 19 additions & 0 deletions ts/src/sourcemapper/sourcemapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ export interface SourceLocation {
name?: string;
line?: number;
column?: number;
/** True when the file declares a sourceMappingURL but the map could not be found. */
missingMapFile?: boolean;
}

/**
Expand Down Expand Up @@ -287,6 +289,8 @@ async function processSourceMap(

export class SourceMapper {
infoMap: Map<string, MapInfoCompiled>;
/** JS files that declared a sourceMappingURL but no map was ultimately found. */
private declaredMissingMap = new Set<string>();
debug: boolean;

static async create(
Expand Down Expand Up @@ -351,6 +355,9 @@ export class SourceMapper {

const limit = createLimiter(CONCURRENCY);

// JS files that declared a sourceMappingURL but Phase 1 couldn't load the map.
const annotatedNotLoaded = new Set<string>();

// Phase 1: Check sourceMappingURL annotations in JS files (higher priority).
await Promise.all(
jsFiles.map(jsPath =>
Expand Down Expand Up @@ -383,6 +390,7 @@ export class SourceMapper {
);
} catch {
// Map file doesn't exist or is unreadable; fall through to Phase 2.
annotatedNotLoaded.add(jsPath);
}
}
}),
Expand All @@ -395,6 +403,14 @@ export class SourceMapper {
limit(() => processSourceMap(this.infoMap, mapPath, this.debug)),
),
);

// Any file whose annotation pointed to a missing map and that still has no
// entry after Phase 2 is tracked as "declared but missing".
for (const jsPath of annotatedNotLoaded) {
if (!this.infoMap.has(jsPath)) {
this.declaredMissingMap.add(jsPath);
}
}
}

private async loadMapContent(
Expand Down Expand Up @@ -480,6 +496,9 @@ export class SourceMapper {
`Source map lookup failed: no map found for ${location.file} (normalized: ${inputPath})`,
);
}
if (this.declaredMissingMap.has(inputPath)) {
return {...location, missingMapFile: true};
}
return location;
}

Expand Down
1 change: 0 additions & 1 deletion ts/test/oom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict';

/* eslint-disable no-console */
import {Worker, isMainThread, threadId} from 'worker_threads';
import {heap} from '../src/index';
import path from 'path';
Expand Down
124 changes: 124 additions & 0 deletions ts/test/test-profile-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,130 @@ describe('profile-serializer', () => {
});
});

describe('missing source map file reporting', () => {
let sourceMapper: SourceMapper;
let missingJsPath: string;

before(async () => {
const fs = await import('fs');
const path = await import('path');
const testDir = tmp.dirSync().name;
missingJsPath = path.join(testDir, 'missing-map.js');
// JS file that declares a sourceMappingURL but the map file doesn't exist.
fs.writeFileSync(
missingJsPath,
'//# sourceMappingURL=nonexistent.js.map\n',
);
sourceMapper = await SourceMapper.create([testDir]);
});

function makeSingleNodeTimeProfile(scriptName: string) {
return {
startTime: 0,
endTime: 1000000,
topDownRoot: {
name: '(root)',
scriptName: 'root',
scriptId: 0,
lineNumber: 0,
columnNumber: 0,
hitCount: 0,
children: [
{
name: 'foo',
scriptName,
scriptId: 1,
lineNumber: 1,
columnNumber: 1,
hitCount: 1,
children: [],
},
],
},
};
}

function assertHasMissingMapToken(profile: Profile) {
const st = profile.stringTable;
const tokenId = st.dedup('dd:has-missing-map-files');
const comments = profile.comment as number[];
assert.ok(
Array.isArray(comments) && comments.includes(tokenId),
'expected dd:has-missing-map-files token in profile comments',
);
}

it('serializeTimeProfile emits missing-map token when a map is declared but absent', () => {
const profile = serializeTimeProfile(
makeSingleNodeTimeProfile(missingJsPath),
1000,
sourceMapper,
);
assertHasMissingMapToken(profile);
});

it('serializeHeapProfile emits missing-map token when a map is declared but absent', () => {
// serialize() iterates root.children, so the node with allocations must
// be a child of the root passed to serializeHeapProfile.
const heapNode = {
name: '(root)',
scriptName: '',
scriptId: 0,
lineNumber: 0,
columnNumber: 0,
allocations: [],
children: [
{
name: 'foo',
scriptName: missingJsPath,
scriptId: 1,
lineNumber: 1,
columnNumber: 1,
allocations: [{sizeBytes: 100, count: 1}],
children: [],
},
],
};
const profile = serializeHeapProfile(
heapNode,
0,
512 * 1024,
undefined,
sourceMapper,
);
assertHasMissingMapToken(profile);
});

it('does not emit missing-map token when no source mapper is used', () => {
const profile = serializeTimeProfile(
makeSingleNodeTimeProfile(missingJsPath),
1000,
);
assert.ok(
!profile.comment || profile.comment.length === 0,
'expected no comments when no source mapper is provided',
);
});

it('does not emit missing-map token when all maps are found', () => {
const {
mapDirPath,
v8TimeGeneratedProfile,
} = require('./profiles-for-tests');
return SourceMapper.create([mapDirPath]).then(sm => {
const profile = serializeTimeProfile(v8TimeGeneratedProfile, 1000, sm);
assert.ok(
!profile.comment || profile.comment.length === 0,
'expected no comments when all maps are resolved',
);
});
});

after(() => {
tmp.setGracefulCleanup();
});
});

describe('source map with column 0 (LineTick simulation)', () => {
// This tests the LEAST_UPPER_BOUND fallback for when V8's LineTick
// doesn't provide column information (column=0)
Expand Down
54 changes: 54 additions & 0 deletions ts/test/test-sourcemapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,58 @@ describe('SourceMapper.loadDirectory', () => {
'expected no mapping to be loaded',
);
});

it('sets missingMapFile=true when sourceMappingURL declares a missing map', async () => {
write('declared-missing.js', '//# sourceMappingURL=nonexistent.js.map\n');
// No declared-missing.js.map written — nothing to fall back to.

const sm = new SourceMapper();
await sm.loadDirectory(tmpDir);

const jsPath = path.join(tmpDir, 'declared-missing.js');
const loc = sm.mappingInfo({file: jsPath, line: 1, column: 0, name: 'foo'});
assert.strictEqual(
loc.missingMapFile,
true,
'expected missingMapFile to be true for a file with a declared but missing map',
);
});

it('does not set missingMapFile when file has no sourceMappingURL', async () => {
write('plain.js', 'console.log("hello");\n');

const sm = new SourceMapper();
await sm.loadDirectory(tmpDir);

const jsPath = path.join(tmpDir, 'plain.js');
const loc = sm.mappingInfo({file: jsPath, line: 1, column: 0, name: 'foo'});
assert.ok(
!loc.missingMapFile,
'expected missingMapFile to be falsy for a file with no sourceMappingURL',
);
});

it('does not set missingMapFile when map was found via .map fallback', async () => {
// JS with annotation pointing to nonexistent path, but a .map file exists
// alongside it (Phase 2 fallback).
const {SourceMapGenerator} = await import('source-map');
const gen = new SourceMapGenerator({file: 'fallback.js'});
gen.addMapping({
source: path.join(tmpDir, 'source.ts'),
generated: {line: 1, column: 0},
original: {line: 10, column: 0},
});
write('fallback.js', '//# sourceMappingURL=nowhere.js.map\n');
write('fallback.js.map', gen.toString());

const sm = new SourceMapper();
await sm.loadDirectory(tmpDir);

const jsPath = path.join(tmpDir, 'fallback.js');
const loc = sm.mappingInfo({file: jsPath, line: 1, column: 0, name: 'foo'});
assert.ok(
!loc.missingMapFile,
'expected missingMapFile to be falsy when map was found via Phase 2 fallback',
);
});
});
1 change: 0 additions & 1 deletion ts/test/test-worker-threads.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line n/no-unsupported-features/node-builtins
import {execFile} from 'child_process';
import {promisify} from 'util';
import {Worker} from 'worker_threads';
Expand Down
Loading