Skip to content

Commit 8b587c9

Browse files
committed
refactor: use a lazy profile annotation collection for lenses
Works towards #10
1 parent de59443 commit 8b587c9

File tree

5 files changed

+192
-167
lines changed

5 files changed

+192
-167
lines changed

packages/vscode-js-profile-core/src/array.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,19 @@ export const toggleInSet = <T>(set: ReadonlySet<T>, value: T) => {
5757

5858
return next;
5959
};
60+
61+
const unset = Symbol('unset');
62+
63+
/**
64+
* Caches the results of the first function call.
65+
*/
66+
export const once = <T>(fn: () => T) => {
67+
let value: T | typeof unset = unset;
68+
return () => {
69+
if (value === unset) {
70+
value = fn();
71+
}
72+
73+
return value;
74+
};
75+
};

packages/vscode-js-profile-core/src/cpu/editorProvider.ts

Lines changed: 11 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,12 @@
55
import * as vscode from 'vscode';
66
import { ICpuProfileRaw, Message } from './types';
77
import { bundlePage } from '../bundlePage';
8-
import { buildModel, IProfileModel, ILocation } from './model';
9-
import { LensCollection } from '../lens-collection';
8+
import { buildModel, IProfileModel } from './model';
109
import { ProfileCodeLensProvider } from '../profileCodeLensProvider';
1110
import { reopenWithEditor } from '../reopenWithEditor';
12-
import { openLocation, getCandidateDiskPaths } from '../open-location';
11+
import { openLocation } from '../open-location';
1312
import { ReadonlyCustomDocument } from '../readonly-custom-document';
14-
15-
const decimalFormat = new Intl.NumberFormat(undefined, {
16-
maximumFractionDigits: 2,
17-
minimumFractionDigits: 2,
18-
});
19-
20-
const integerFormat = new Intl.NumberFormat(undefined, {
21-
maximumFractionDigits: 0,
22-
});
13+
import { ProfileAnnotations } from '../profileAnnotations';
2314

2415
export class CpuProfileEditorProvider
2516
implements vscode.CustomEditorProvider<ReadonlyCustomDocument<IProfileModel>> {
@@ -34,7 +25,14 @@ export class CpuProfileEditorProvider
3425
const content = await vscode.workspace.fs.readFile(uri);
3526
const raw: ICpuProfileRaw = JSON.parse(content.toString());
3627
const document = new ReadonlyCustomDocument(uri, buildModel(raw));
37-
this.lens.registerLenses(this.createLensCollection(document));
28+
29+
const annotations = new ProfileAnnotations();
30+
const rootPath = document.userData.rootPath;
31+
for (const location of document.userData.locations) {
32+
annotations.add(rootPath, location);
33+
}
34+
35+
this.lens.registerLenses(annotations);
3836
return document;
3937
}
4038

@@ -99,56 +97,4 @@ export class CpuProfileEditorProvider
9997
) {
10098
return vscode.workspace.fs.copy(document.uri, destination, { overwrite: true });
10199
}
102-
103-
private createLensCollection(document: ReadonlyCustomDocument<IProfileModel>) {
104-
type LensData = { self: number; agg: number; ticks: number };
105-
106-
const lenses = new LensCollection<LensData>(dto => {
107-
let title: string;
108-
if (dto.self > 10 || dto.agg > 10) {
109-
title =
110-
`${decimalFormat.format(dto.self / 1000)}ms Self Time, ` +
111-
`${decimalFormat.format(dto.agg / 1000)}ms Total`;
112-
} else if (dto.ticks) {
113-
title = `${integerFormat.format(dto.ticks)} Ticks`;
114-
} else {
115-
return;
116-
}
117-
118-
return { command: '', title };
119-
});
120-
121-
const merge = (location: ILocation) => (existing?: LensData) => ({
122-
ticks: (existing?.ticks || 0) + location.ticks,
123-
self: (existing?.self || 0) + location.selfTime,
124-
agg: (existing?.agg || 0) + location.aggregateTime,
125-
});
126-
127-
for (const location of document.userData?.locations || []) {
128-
const mergeFn = merge(location);
129-
lenses.set(
130-
location.callFrame.url,
131-
new vscode.Position(
132-
Math.max(0, location.callFrame.lineNumber),
133-
Math.max(0, location.callFrame.columnNumber),
134-
),
135-
mergeFn,
136-
);
137-
138-
const src = location.src;
139-
if (!src || src.source.sourceReference !== 0 || !src.source.path) {
140-
continue;
141-
}
142-
143-
for (const path of getCandidateDiskPaths(document.userData?.rootPath, src.source)) {
144-
lenses.set(
145-
path,
146-
new vscode.Position(Math.max(0, src.lineNumber - 1), Math.max(0, src.columnNumber - 1)),
147-
mergeFn,
148-
);
149-
}
150-
}
151-
152-
return lenses;
153-
}
154100
}

packages/vscode-js-profile-core/src/lens-collection.ts

Lines changed: 0 additions & 73 deletions
This file was deleted.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { CodeLens, Range, Position } from 'vscode';
6+
import { lowerCaseInsensitivePath } from './path';
7+
import { ILocation } from './cpu/model';
8+
import { once } from './array';
9+
import { getCandidateDiskPaths } from './open-location';
10+
11+
export interface IProfileInformation {
12+
selfTime: number;
13+
aggregateTime: number;
14+
ticks: number;
15+
}
16+
17+
const decimalFormat = new Intl.NumberFormat(undefined, {
18+
maximumFractionDigits: 2,
19+
minimumFractionDigits: 2,
20+
});
21+
22+
const basenameRe = /[^/\\]+$/;
23+
const getBasename = (pathOrUrl: string) => basenameRe.exec(pathOrUrl)?.[0] ?? pathOrUrl;
24+
25+
/**
26+
* A collection of profile data. Paths are expanded lazily, as doing so
27+
* up-front for very large profiles turned out to be costly (mainly in path)
28+
* manipulation.
29+
*/
30+
export class ProfileAnnotations {
31+
private readonly basenamesToExpand = new Map<string, (() => void)[]>();
32+
private readonly data = new Map<string, { position: Position; data: IProfileInformation }[]>();
33+
34+
public add(rootPath: string | undefined, location: ILocation) {
35+
const expand = once(() => {
36+
this.set(
37+
location.callFrame.url,
38+
new Position(
39+
Math.max(0, location.callFrame.lineNumber),
40+
Math.max(0, location.callFrame.columnNumber),
41+
),
42+
location,
43+
);
44+
45+
const src = location.src;
46+
if (!src || src.source.sourceReference !== 0 || !src.source.path) {
47+
return;
48+
}
49+
50+
for (const path of getCandidateDiskPaths(rootPath, src.source)) {
51+
this.set(
52+
path,
53+
new Position(Math.max(0, src.lineNumber - 1), Math.max(0, src.columnNumber - 1)),
54+
location,
55+
);
56+
}
57+
});
58+
59+
this.addExpansionFn(getBasename(location.callFrame.url), expand);
60+
if (location.src?.source.path) {
61+
this.addExpansionFn(getBasename(location.src.source.path), expand);
62+
}
63+
}
64+
65+
/**
66+
* Adds a function to expand performance data for the given location.
67+
*/
68+
private addExpansionFn(basename: string, expand: () => void) {
69+
let arr = this.basenamesToExpand.get(basename);
70+
if (!arr) {
71+
arr = [];
72+
this.basenamesToExpand.set(basename, arr);
73+
}
74+
75+
arr.push(expand);
76+
}
77+
78+
/**
79+
* Adds a new code lens at the given location in the file.
80+
*/
81+
private set(file: string, position: Position, data: ILocation) {
82+
let list = this.data.get(lowerCaseInsensitivePath(file));
83+
if (!list) {
84+
list = [];
85+
this.data.set(lowerCaseInsensitivePath(file), list);
86+
}
87+
88+
let index = 0;
89+
while (index < list.length && list[index].position.line < position.line) {
90+
index++;
91+
}
92+
93+
if (list[index]?.position.line === position.line) {
94+
const existing = list[index];
95+
if (position.character < existing.position.character) {
96+
existing.position = new Position(position.line, position.character);
97+
}
98+
existing.data.aggregateTime += data.aggregateTime;
99+
existing.data.selfTime += data.selfTime;
100+
existing.data.ticks += data.ticks;
101+
} else {
102+
list.splice(index, 0, {
103+
position: new Position(position.line, position.character),
104+
data: {
105+
aggregateTime: data.aggregateTime,
106+
selfTime: data.selfTime,
107+
ticks: data.ticks,
108+
},
109+
});
110+
}
111+
}
112+
113+
/**
114+
* Get all lenses for a file. Ordered by line number.
115+
*/
116+
public getLensesForFile(file: string): CodeLens[] {
117+
this.expandForFile(file);
118+
119+
return (
120+
this.data
121+
.get(lowerCaseInsensitivePath(file))
122+
?.map(({ position, data }) => {
123+
if (data.aggregateTime === 0 && data.selfTime === 0) {
124+
return [];
125+
}
126+
127+
const range = new Range(position, position);
128+
return [
129+
new CodeLens(range, {
130+
title:
131+
`${decimalFormat.format(data.selfTime / 1000)}ms Self Time, ` +
132+
`${decimalFormat.format(data.aggregateTime / 1000)}ms Total`,
133+
command: '',
134+
}),
135+
new CodeLens(range, {
136+
title: 'Clear',
137+
command: 'extension.jsProfileVisualizer.table.clearCodeLenses',
138+
}),
139+
];
140+
})
141+
.reduce((acc, lenses) => [...acc, ...lenses], []) ?? []
142+
);
143+
}
144+
145+
private expandForFile(file: string) {
146+
const basename = getBasename(file);
147+
const fns = this.basenamesToExpand.get(basename);
148+
if (!fns) {
149+
return;
150+
}
151+
152+
for (const fn of fns) {
153+
fn();
154+
}
155+
156+
this.basenamesToExpand.delete(basename);
157+
}
158+
}

0 commit comments

Comments
 (0)