porffor 0.57.14 → 0.57.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compiler/assemble.js +1 -1
- package/compiler/builtins/_internal_object.ts +10 -113
- package/compiler/builtins_precompiled.js +22 -22
- package/compiler/codegen.js +4 -1
- package/compiler/disassemble.js +3 -2
- package/compiler/pgo.js +2 -2
- package/compiler/precompile.js +8 -2
- package/foo.js +24 -8
- package/package.json +1 -1
- package/runtime/flamegraph.js +441 -158
- package/runtime/hotlines.js +3 -3
- package/runtime/index.js +1 -1
package/runtime/flamegraph.js
CHANGED
@@ -4,200 +4,483 @@ import { number } from '../compiler/encoding.js';
|
|
4
4
|
import { importedFuncs } from '../compiler/builtins.js';
|
5
5
|
import compile, { createImport } from '../compiler/wrap.js';
|
6
6
|
import fs from 'node:fs';
|
7
|
+
import http from 'node:http';
|
8
|
+
import path from 'node:path';
|
9
|
+
import url from 'node:url';
|
7
10
|
|
8
11
|
const file = process.argv.slice(2).find(x => x[0] !== '-');
|
12
|
+
if (!file) {
|
13
|
+
console.error('Usage: node flamegraph.js <file.js>');
|
14
|
+
process.exit(1);
|
15
|
+
}
|
9
16
|
let source = fs.readFileSync(file, 'utf8');
|
10
17
|
|
11
|
-
const samplesFunc = [];
|
12
|
-
const samplesStart = [];
|
13
|
-
const samplesEnd = [];
|
18
|
+
const samplesFunc = []; // Stores function index for each sample
|
19
|
+
const samplesStart = []; // Stores start timestamp for each sample
|
20
|
+
const samplesEnd = []; // Stores end timestamp for each sample (filled in profile2)
|
21
|
+
let funcLookup = new Map(); // Maps function index to { index, name, internal, ... }
|
22
|
+
let start, end; // Overall start and end time of execution
|
14
23
|
|
15
|
-
|
16
|
-
const
|
17
|
-
|
18
|
-
let start, end;
|
19
|
-
const noAnsi = s => s.replace(/\u001b\[[0-9]+m/g, '');
|
20
|
-
const controls = {
|
21
|
-
};
|
22
|
-
|
23
|
-
const controlInfo = Object.keys(controls).reduce((acc, x, i) => acc + `\x1B[45m\x1B[97m${x}\x1b[105m\x1b[37m ${controls[x]} `, '');
|
24
|
-
const plainControlInfo = noAnsi(controlInfo);
|
24
|
+
// --- Performance Tuning ---
|
25
|
+
const minSampleDurationMs = 0.01; // Ignore samples shorter than this for flame graph hierarchy
|
25
26
|
|
27
|
+
// --- Spinner Globals ---
|
26
28
|
const spinner = ['-', '\\', '|', '/'];
|
27
|
-
let
|
29
|
+
let spinIdx = 0;
|
30
|
+
let lastProgressUpdate = 0;
|
31
|
+
const progressUpdateIntervalMs = 500; // Update every 500ms
|
28
32
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
// process.on('exit', onExit);
|
33
|
-
// process.on('SIGINT', onExit);
|
34
|
-
|
35
|
-
let lastRenderTime = 0;
|
36
|
-
const render = () => {
|
37
|
-
const renderStart = performance.now();
|
38
|
-
process.stdout.write('\x1b[?2026h\x1b[1;1H\x1b[J\x1b[?25l');
|
39
|
-
|
40
|
-
let text = ' ';
|
41
|
-
if (!end) {
|
42
|
-
text += `${spinner[spin++ % 4]} `;
|
43
|
-
} else {
|
44
|
-
text += ' ';
|
45
|
-
}
|
33
|
+
// Stack tracking for flamegraph construction
|
34
|
+
let running = new Uint32Array(1024); // Stores indices into samplesFunc/Start/End arrays
|
35
|
+
let runningIdx = 0;
|
46
36
|
|
37
|
+
// --- Profiling Hooks ---
|
38
|
+
createImport('profile1', [ Valtype.i32 ], 0, f => { // pre-call
|
47
39
|
const now = performance.now();
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
const btHeight = 40;
|
59
|
-
const fgBottom = termHeight - btHeight - 10;
|
60
|
-
|
61
|
-
let lastEnds = [];
|
62
|
-
let timelineEnd = total;
|
63
|
-
|
64
|
-
const xScale = x => 1 + (((x - start) / timelineEnd) * (termWidth - 2));
|
65
|
-
const draw = (func, start, end, running = false) => {
|
66
|
-
let depth = lastEnds.length;
|
67
|
-
lastEnds.push(end);
|
68
|
-
|
69
|
-
let color = '103';
|
70
|
-
if (func.internal) color = '105';
|
71
|
-
|
72
|
-
start = xScale(start) | 0;
|
73
|
-
|
74
|
-
end = xScale(end) | 0;
|
75
|
-
if (end >= termWidth) end = termWidth - 1;
|
76
|
-
|
77
|
-
const width = end - start;
|
78
|
-
if (start >= termWidth || width === 0) return;
|
79
|
-
|
80
|
-
let text = func.name;
|
81
|
-
if (text.length > width) text = width < 5 ? ' '.repeat(width) : (text.slice(0, width - 1) + '…');
|
82
|
-
if (text.length < width) text += ' '.repeat(width - text.length);
|
83
|
-
|
84
|
-
let y = fgBottom - depth;
|
85
|
-
process.stdout.write(`\x1b[${1 + y};${1 + start}H\x1b[51m\x1b[30m\x1b[${color}m${text}`);
|
86
|
-
};
|
87
|
-
|
88
|
-
const funcTotalTaken = new Map(), funcMeta = new Map();
|
89
|
-
for (let i = 0; i < samples; i++) {
|
90
|
-
const func = funcLookup.get(samplesFunc[i]);
|
91
|
-
const start = samplesStart[i];
|
92
|
-
const end = samplesEnd[i] ?? now;
|
93
|
-
|
94
|
-
// DD
|
95
|
-
// BBCCEE
|
96
|
-
// AAAAAAAA FFFF
|
97
|
-
while (start > lastEnds.at(-1)) lastEnds.pop();
|
98
|
-
|
99
|
-
draw(func, start, end);
|
100
|
-
|
101
|
-
if (end == now) continue;
|
102
|
-
const taken = end - start;
|
103
|
-
funcTotalTaken.set(func.index, (funcTotalTaken.get(func.index) ?? 0) + taken);
|
104
|
-
|
105
|
-
if (!funcMeta.has(func.index)) funcMeta.set(func.index, [0, Infinity, -Infinity]);
|
106
|
-
const meta = funcMeta.get(func.index);
|
107
|
-
meta[0]++;
|
108
|
-
|
109
|
-
if (meta[1] > taken) meta[1] = taken;
|
110
|
-
if (taken > meta[2]) meta[2] = taken;
|
111
|
-
}
|
112
|
-
|
113
|
-
process.stdout.write(`\x1b[${termHeight - btHeight};1H\x1b[0m\x1b[90m${'▁'.repeat(termWidth)}\n`);
|
114
|
-
|
115
|
-
(() => {
|
116
|
-
const perTime = 18;
|
117
|
-
let text = ' ' + 'name';
|
118
|
-
text += `${' '.repeat(60 - text.length)}┃ total`;
|
119
|
-
text += `${' '.repeat(60 + 5 + perTime - text.length)}┃ min`;
|
120
|
-
text += `${' '.repeat(60 + 5 + (perTime * 2) - text.length)}┃ avg`;
|
121
|
-
text += `${' '.repeat(60 + 5 + (perTime * 3) - text.length)}┃ max`;
|
122
|
-
text += `${' '.repeat(60 + 5 + (perTime * 4) - text.length)}┃ count`;
|
123
|
-
process.stdout.write(`\x1b[0m\x1b[2m${text.replaceAll('┃', '\x1b[0m\x1b[90m┃\x1b[0m\x1b[2m')}${' '.repeat(termWidth - text.length)}\x1b[0m`);
|
124
|
-
})();
|
125
|
-
|
126
|
-
const topTakenFuncs = [...funcTotalTaken.keys()].sort((a, b) => funcTotalTaken.get(b) - funcTotalTaken.get(a));
|
127
|
-
for (let i = 0; i < btHeight - 2; i++) {
|
128
|
-
const func = funcLookup.get(topTakenFuncs[i]);
|
129
|
-
if (!func) continue;
|
130
|
-
|
131
|
-
const total = funcTotalTaken.get(func.index);
|
132
|
-
const [ count, min, max ] = funcMeta.get(func.index);
|
133
|
-
const avg = total / count;
|
134
|
-
|
135
|
-
const perTime = 18;
|
136
|
-
let text = ' \x1b[1m' + func.name + '\x1b[22m';
|
137
|
-
text += `${' '.repeat(69 - text.length)}┃ ${total.toFixed(2)}ms`;
|
138
|
-
text += `${' '.repeat(69 + perTime - text.length)}${((total / _total) * 100).toFixed(0)}%`;
|
139
|
-
text += `${' '.repeat(69 + 5 + perTime - text.length)}┃ ${min.toFixed(2)}ms`;
|
140
|
-
text += `${' '.repeat(69 + 5 + (perTime * 2) - text.length)}┃ ${avg.toFixed(2)}ms`;
|
141
|
-
text += `${' '.repeat(69 + 5 + (perTime * 3) - text.length)}┃ ${max.toFixed(2)}ms`;
|
142
|
-
text += `${' '.repeat(69 + 5 + (perTime * 4) - text.length)}┃ ${count}`;
|
143
|
-
process.stdout.write(`\x1b[${termHeight - btHeight + 2 + i};1H\x1b[0m${text.replaceAll('┃', '\x1b[90m┃\x1b[0m').replaceAll(/(\.[0-9][0-9])ms/g, '\x1b[2m$1ms\x1b[22m').replaceAll('%', '\x1b[2m%\x1b[22m')}${' '.repeat(termWidth - noAnsi(text).length)}`);
|
144
|
-
}
|
40
|
+
samplesStart.push(now);
|
41
|
+
// Store the *index* of the just pushed start time/func id
|
42
|
+
// This index corresponds to the entry in samplesFunc/Start/End
|
43
|
+
const sampleIndex = samplesFunc.push(f) - 1;
|
44
|
+
samplesEnd.push(null); // Placeholder for end time
|
45
|
+
if (runningIdx >= running.length) {
|
46
|
+
// Resize running buffer if needed
|
47
|
+
const newRunning = new Uint32Array(running.length * 2);
|
48
|
+
newRunning.set(running);
|
49
|
+
running = newRunning;
|
145
50
|
}
|
146
|
-
|
147
|
-
process.stdout.write(`\x1b[${termHeight};1H\x1b[107m\x1b[30m${text}${' '.repeat(termWidth - plainControlInfo.length - noAnsi(text).length - 1)}${controlInfo} \x1b[0m\x1b[?2026l`);
|
148
|
-
lastRenderTime = performance.now() - renderStart;
|
149
|
-
};
|
150
|
-
|
151
|
-
createImport('profile1', 1, 0, f => { // pre-call
|
152
|
-
samplesStart.push(performance.now());
|
153
|
-
running[runningIdx++] = samplesFunc.push(f) - 1;
|
51
|
+
running[runningIdx++] = sampleIndex;
|
154
52
|
});
|
155
|
-
|
53
|
+
|
54
|
+
createImport('profile2', 0, 0, () => { // post-call
|
156
55
|
const now = performance.now();
|
157
|
-
|
56
|
+
if (runningIdx > 0) {
|
57
|
+
const sampleIndex = running[--runningIdx];
|
58
|
+
// Only set end time if it hasn't been set (handles potential async overlaps?)
|
59
|
+
if (samplesEnd[sampleIndex] === null) {
|
60
|
+
samplesEnd[sampleIndex] = now;
|
61
|
+
}
|
62
|
+
}
|
158
63
|
|
159
|
-
if
|
160
|
-
|
161
|
-
|
64
|
+
// Check if it's time to update progress spinner
|
65
|
+
if (now - lastProgressUpdate > progressUpdateIntervalMs) {
|
66
|
+
lastProgressUpdate = now;
|
67
|
+
const sampleCount = samplesFunc.length;
|
68
|
+
const currentSpinner = spinner[spinIdx++ % spinner.length];
|
69
|
+
const termWidth = process.stdout.columns || 80;
|
70
|
+
const output = `\r${currentSpinner} Running... Samples: ${sampleCount}`;
|
71
|
+
// Write progress, ensuring the rest of the line is cleared
|
72
|
+
process.stdout.write(output + ' '.repeat(Math.max(0, termWidth - output.length - 1)));
|
162
73
|
}
|
163
74
|
});
|
164
75
|
|
165
|
-
|
166
|
-
|
76
|
+
// --- Compilation ---
|
77
|
+
Prefs.treeshakeWasmImports = false; // Keep profile imports
|
167
78
|
globalThis.compileCallback = ({ funcs }) => {
|
79
|
+
funcLookup = new Map(); // Reset map
|
168
80
|
for (const x of funcs) {
|
169
81
|
funcLookup.set(x.index, x);
|
170
82
|
|
83
|
+
// Inject profiling calls around existing calls
|
171
84
|
const w = x.wasm;
|
172
85
|
for (let i = 0; i < w.length; i++) {
|
173
86
|
if (w[i][0] === Opcodes.call) {
|
174
87
|
const f = w[i][1];
|
88
|
+
// Don't profile calls to imported funcs (like profile1/2 itself)
|
175
89
|
if (f < importedFuncs.length) continue;
|
176
90
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
} else {
|
181
|
-
local = x.localInd++;
|
182
|
-
x.locals['#profile_tmp'] = { idx: local, type: Valtype.f64 };
|
183
|
-
}
|
184
|
-
|
185
|
-
w.splice(i + 1, 0, number(f, Valtype.i32), [ Opcodes.call, importedFuncs.profile2 ]);
|
91
|
+
// Inject profile2 *after* the call
|
92
|
+
w.splice(i + 1, 0, [ Opcodes.call, importedFuncs.profile2 ]);
|
93
|
+
// Inject function index push and profile1 *before* the call
|
186
94
|
w.splice(i, 0, number(f, Valtype.i32), [ Opcodes.call, importedFuncs.profile1 ]);
|
187
|
-
i +=
|
95
|
+
i += 3; // Skip the 3 instructions we just added
|
188
96
|
}
|
189
97
|
}
|
190
98
|
}
|
191
99
|
};
|
192
100
|
|
193
|
-
|
194
|
-
let running = new Uint32Array(1024), runningIdx = 0;
|
101
|
+
console.log('Compiling...');
|
195
102
|
const { exports } = compile(source, undefined, {}, () => {});
|
196
103
|
|
104
|
+
// --- Execution with Progress Spinner ---
|
105
|
+
|
106
|
+
console.log('Starting execution...');
|
107
|
+
// Initial placeholder message
|
108
|
+
const initialMsg = "Running... (waiting for first samples)";
|
109
|
+
const termWidthInitial = process.stdout.columns || 80;
|
110
|
+
process.stdout.write('\r' + initialMsg + ' '.repeat(Math.max(0, termWidthInitial - initialMsg.length -1)));
|
111
|
+
|
197
112
|
start = performance.now();
|
198
|
-
render();
|
199
113
|
|
200
|
-
|
114
|
+
try {
|
115
|
+
exports.main();
|
116
|
+
} finally {
|
117
|
+
// Clear the spinner line
|
118
|
+
const termWidthFinal = process.stdout.columns || 80;
|
119
|
+
process.stdout.write('\r' + ' '.repeat(termWidthFinal) + '\r');
|
120
|
+
}
|
121
|
+
|
201
122
|
end = performance.now();
|
123
|
+
console.log(`Execution finished in ${(end - start).toFixed(2)}ms`);
|
124
|
+
|
125
|
+
// --- Data Processing ---
|
126
|
+
|
127
|
+
const totalDuration = end - start;
|
128
|
+
// const processedSamples = []; // Remove: We'll create a filtered list directly
|
129
|
+
const filteredSamples = []; // Samples that meet the duration threshold for the flame graph
|
130
|
+
const funcStats = new Map(); // { index -> { total: 0, count: 0, min: Infinity, max: -Infinity, name: '', internal: false }}
|
131
|
+
|
132
|
+
// Process raw samples: Calculate stats for all, filter for flame graph
|
133
|
+
console.log(`Processing ${samplesFunc.length} raw samples...`);
|
134
|
+
let samplesBelowThreshold = 0;
|
135
|
+
for (let i = 0; i < samplesFunc.length; i++) {
|
136
|
+
const funcIndex = samplesFunc[i];
|
137
|
+
const func = funcLookup.get(funcIndex);
|
138
|
+
const funcName = func ? func.name : `unknown_${funcIndex}`;
|
139
|
+
const isInternal = func ? !!func.internal : false; // Read internal flag
|
140
|
+
const startTime = samplesStart[i];
|
141
|
+
const endTime = samplesEnd[i] === null ? end : samplesEnd[i]; // Cap duration
|
142
|
+
const duration = endTime - startTime;
|
143
|
+
|
144
|
+
if (duration < 0) continue; // Skip potentially erroneous samples
|
145
|
+
|
146
|
+
// --- Update function stats (always do this) ---
|
147
|
+
if (!funcStats.has(funcIndex)) {
|
148
|
+
funcStats.set(funcIndex, { total: 0, count: 0, min: Infinity, max: -Infinity, name: funcName, internal: isInternal }); // Store internal flag
|
149
|
+
}
|
150
|
+
const stats = funcStats.get(funcIndex);
|
151
|
+
stats.total += duration;
|
152
|
+
stats.count++;
|
153
|
+
if (duration < stats.min) stats.min = duration;
|
154
|
+
if (duration > stats.max) stats.max = duration;
|
155
|
+
|
156
|
+
// --- Filter samples for flame graph hierarchy ---
|
157
|
+
if (duration >= minSampleDurationMs) {
|
158
|
+
filteredSamples.push({
|
159
|
+
// Only store data needed for buildHierarchy
|
160
|
+
name: funcName,
|
161
|
+
start: startTime - start, // Relative to overall start
|
162
|
+
end: endTime - start, // Relative to overall start
|
163
|
+
duration: duration,
|
164
|
+
internal: isInternal // Store internal flag for flamegraph nodes
|
165
|
+
});
|
166
|
+
} else {
|
167
|
+
samplesBelowThreshold++;
|
168
|
+
}
|
169
|
+
}
|
170
|
+
console.log(`Filtered out ${samplesBelowThreshold} samples shorter than ${minSampleDurationMs}ms.`);
|
171
|
+
console.log(`Building hierarchy from ${filteredSamples.length} samples...`);
|
172
|
+
|
173
|
+
// --- d3-flame-graph Data Generation ---
|
174
|
+
// Requires a hierarchical structure: { name: 'root', value: total, children: [...] }
|
175
|
+
// where value represents the total time (inclusive of children)
|
176
|
+
function buildHierarchy(samples) {
|
177
|
+
if (!samples || samples.length === 0) {
|
178
|
+
return { name: path.basename(file), value: 0, children: [], internal: false }; // Root is not internal
|
179
|
+
}
|
180
|
+
|
181
|
+
// Sort primarily by start time, secondarily by end time descending (parents first)
|
182
|
+
samples.sort((a, b) => a.start - b.start || b.end - a.end);
|
183
|
+
|
184
|
+
const root = { name: path.basename(file), value: 0, children: [], internal: false }; // Root is not internal
|
185
|
+
root.startTime = 0;
|
186
|
+
root.endTime = totalDuration;
|
187
|
+
const stack = [{ node: root, startTime: root.startTime, endTime: root.endTime }]; // Consistent structure
|
188
|
+
|
189
|
+
for (const sample of samples) {
|
190
|
+
// Pass internal flag from filteredSample to newNode
|
191
|
+
const newNode = { name: sample.name, value: sample.duration, children: [], internal: sample.internal };
|
192
|
+
const sampleStartTime = sample.start;
|
193
|
+
const sampleEndTime = sample.end;
|
194
|
+
|
195
|
+
// Pop stack until parent is found
|
196
|
+
// Parent must start before or at the same time, and end after or at the same time
|
197
|
+
// Accessing .startTime and .endTime on stack elements is correct here
|
198
|
+
while (stack.length > 1 && (stack[stack.length - 1].startTime > sampleStartTime || stack[stack.length - 1].endTime < sampleEndTime)) {
|
199
|
+
stack.pop();
|
200
|
+
}
|
201
|
+
|
202
|
+
const parent = stack[stack.length - 1];
|
203
|
+
parent.node.children.push(newNode); // Correctly access children via parent.node
|
202
204
|
|
203
|
-
|
205
|
+
// Add node to stack with its *graph node* reference and end time
|
206
|
+
stack.push({ node: newNode, startTime: sampleStartTime, endTime: sampleEndTime });
|
207
|
+
}
|
208
|
+
|
209
|
+
// d3-flamegraph expects `value` to be inclusive time, but we provide durations.
|
210
|
+
// The library handles the aggregation based on children, so we just pass the duration.
|
211
|
+
// Let's ensure root value is the total duration if samples don't cover it
|
212
|
+
// root.value = Math.max(root.value, samples.reduce((sum, s) => sum + s.duration, 0)); // Remove: Not needed with selfValue(true)
|
213
|
+
|
214
|
+
return root;
|
215
|
+
}
|
216
|
+
|
217
|
+
const d3FlameGraphData = buildHierarchy(filteredSamples);
|
218
|
+
|
219
|
+
// --- Bar Chart Data Generation (remains the same) ---
|
220
|
+
const barChartData = Array.from(funcStats.values())
|
221
|
+
.map(stats => ({
|
222
|
+
name: stats.name,
|
223
|
+
total: stats.total,
|
224
|
+
min: stats.min === Infinity ? 0 : stats.min,
|
225
|
+
max: stats.max === -Infinity ? 0 : stats.max,
|
226
|
+
avg: stats.count > 0 ? stats.total / stats.count : 0,
|
227
|
+
count: stats.count,
|
228
|
+
internal: stats.internal // Include internal flag from funcStats
|
229
|
+
}))
|
230
|
+
.sort((a, b) => b.total - a.total) // Sort by total time descending
|
231
|
+
.slice(0, 50); // Limit to top 50 functions
|
232
|
+
|
233
|
+
// --- HTML Generation ---
|
234
|
+
function generateHtml(flameData, chartData) {
|
235
|
+
const flameJson = JSON.stringify(flameData);
|
236
|
+
const chartJson = JSON.stringify(chartData);
|
237
|
+
|
238
|
+
return `
|
239
|
+
<!DOCTYPE html>
|
240
|
+
<html>
|
241
|
+
<head>
|
242
|
+
<meta charset="utf-8">
|
243
|
+
<title>Porffor Profile: ${path.basename(file)}</title>
|
244
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.css">
|
245
|
+
<style>
|
246
|
+
@import url(https://fonts.bunny.net/css?family=jetbrains-mono:400,600,800);
|
247
|
+
|
248
|
+
:root {
|
249
|
+
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
250
|
+
--header-primary: #ffffff;
|
251
|
+
--header-secondary: #b9bbbe;
|
252
|
+
--text-normal: #ffffff;
|
253
|
+
--text-muted: #d0d4d8;
|
254
|
+
--accent-dark: #3e2066;
|
255
|
+
--accent: #8545cf;
|
256
|
+
--accent-light: #9c60e0;
|
257
|
+
--background-primary: #100420;
|
258
|
+
--background-secondary: #200840;
|
259
|
+
}
|
260
|
+
html, body {
|
261
|
+
margin: 0;
|
262
|
+
padding: 0;
|
263
|
+
}
|
264
|
+
body {
|
265
|
+
font-family: var(--font-family);
|
266
|
+
margin: 16px;
|
267
|
+
background-color: var(--background-primary);
|
268
|
+
color: var(--text-muted);
|
269
|
+
}
|
270
|
+
h2 {
|
271
|
+
text-align: center;
|
272
|
+
color: var(--header-primary);
|
273
|
+
font-weight: 800;
|
274
|
+
margin: 0;
|
275
|
+
margin-bottom: 16px;
|
276
|
+
font-size: 24px;
|
277
|
+
}
|
278
|
+
#flamegraph-container, #barchart-container {
|
279
|
+
border: 2px solid var(--accent);
|
280
|
+
margin-bottom: 32px;
|
281
|
+
background-color: var(--background-secondary);
|
282
|
+
padding: 16px;
|
283
|
+
border-radius: 0;
|
284
|
+
}
|
285
|
+
#flamegraph-details {
|
286
|
+
margin-top: 10px;
|
287
|
+
font-size: 14px;
|
288
|
+
color: var(--text-muted);
|
289
|
+
min-height: 2em;
|
290
|
+
}
|
291
|
+
.chart-title {
|
292
|
+
font-size: 20px;
|
293
|
+
font-weight: 600;
|
294
|
+
margin-bottom: 15px;
|
295
|
+
color: var(--header-primary);
|
296
|
+
border-bottom: 1px solid var(--accent);
|
297
|
+
padding-bottom: 8px;
|
298
|
+
margin-top: 0;
|
299
|
+
}
|
300
|
+
.bar-row { display: flex; align-items: center; font-size: 14px; white-space: nowrap; height: 24px; }
|
301
|
+
.bar-label { width: 320px; overflow: hidden; text-overflow: ellipsis; padding-right: 10px; color: var(--text-muted); font-weight: 600;}
|
302
|
+
.bar-rect-bg { flex-grow: 1; background-color: var(--background-secondary); border: 1px solid var(--accent-dark); height: 100%; position: relative; min-width: 100px; border-radius: 0; }
|
303
|
+
.bar-rect { background-color: var(--accent); height: 100%; position: absolute; left: 0; top: 0; border-radius: 0; display: flex; align-items: center; }
|
304
|
+
.bar-value { color: var(--header-primary); font-size: 14px; position: relative; left: 6px; }
|
305
|
+
.bar-stats-inline { position: absolute; right: 8px; top: 0; height: 100%; display: flex; align-items: center; font-size: 12px; color: var(--header-secondary); }
|
306
|
+
#flamegraph-chart {
|
307
|
+
width: 100%;
|
308
|
+
background-color: var(--background-secondary);
|
309
|
+
min-height: 400px;
|
310
|
+
}
|
311
|
+
.d3-flame-graph rect {
|
312
|
+
stroke: var(--background-primary);
|
313
|
+
stroke-width: 0.5;
|
314
|
+
fill-opacity: .9;
|
315
|
+
}
|
316
|
+
.d3-flame-graph rect:hover {
|
317
|
+
stroke: var(--accent-light);
|
318
|
+
stroke-width: 1;
|
319
|
+
fill-opacity: 1;
|
320
|
+
}
|
321
|
+
.d3-flame-graph-label {
|
322
|
+
user-select: none;
|
323
|
+
/* white-space: unset; */
|
324
|
+
/* text-overflow: unset; */
|
325
|
+
overflow: hidden;
|
326
|
+
font-size: 12px;
|
327
|
+
font-family: inherit;
|
328
|
+
/* padding: 0 0 0; */
|
329
|
+
color: black;
|
330
|
+
/* overflow-wrap: break-word; */
|
331
|
+
line-height: 1.5;
|
332
|
+
}
|
333
|
+
.d3-flame-graph .depth-0 .d3-flame-graph-label {
|
334
|
+
font-size: 16px;
|
335
|
+
}
|
336
|
+
</style>
|
337
|
+
</head>
|
338
|
+
<body>
|
339
|
+
<h2>Porffor Profile: ${path.basename(file)} (${totalDuration.toFixed(2)}ms total)</h2>
|
340
|
+
|
341
|
+
<div id="flamegraph-container" style="position: relative;">
|
342
|
+
<div id="flamegraph-chart"></div>
|
343
|
+
<div id="flamegraph-details"></div>
|
344
|
+
</div>
|
345
|
+
|
346
|
+
<div id="barchart-container">
|
347
|
+
<div class="chart-title">Function Execution Time</div>
|
348
|
+
<div id="barchart"></div>
|
349
|
+
</div>
|
350
|
+
|
351
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
352
|
+
<script src="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.min.js"></script>
|
353
|
+
<script>
|
354
|
+
const flameData = ${flameJson};
|
355
|
+
const chartData = ${chartJson};
|
356
|
+
const totalValue = flameData.value;
|
357
|
+
|
358
|
+
const flamegraphContainer = document.getElementById('flamegraph-chart');
|
359
|
+
const detailsElement = document.getElementById('flamegraph-details');
|
360
|
+
const graphWidth = flamegraphContainer.clientWidth || 960;
|
361
|
+
const graphHeight = flamegraphContainer.clientHeight || 400;
|
362
|
+
|
363
|
+
const chartDataByName = chartData.reduce((acc, d) => {
|
364
|
+
acc[d.name] = d;
|
365
|
+
return acc;
|
366
|
+
}, {});
|
367
|
+
|
368
|
+
var flameChart = flamegraph()
|
369
|
+
.width(graphWidth)
|
370
|
+
.height(graphHeight)
|
371
|
+
.cellHeight(18)
|
372
|
+
.transitionDuration(0)
|
373
|
+
.minFrameSize(1)
|
374
|
+
.sort(true)
|
375
|
+
.selfValue(true)
|
376
|
+
.setDetailsElement(detailsElement)
|
377
|
+
.setColorHue('warm')
|
378
|
+
// Add a color mapper to dim internal functions
|
379
|
+
.setColorMapper(function(d, originalColor) {
|
380
|
+
if (d.data.internal) {
|
381
|
+
return 'var(--accent-light)';
|
382
|
+
}
|
383
|
+
return originalColor; // Return original color if not internal or parsing failed
|
384
|
+
})
|
385
|
+
.label(function(d) {
|
386
|
+
if (d.data.name === 'root') return '';
|
387
|
+
|
388
|
+
return \`\${d.data.name}: \${d.value.toFixed(2)}ms (\${(d.value / (chartDataByName[d.data.name]?.avg ?? d.value)).toFixed(2)}x avg)\`;
|
389
|
+
});
|
390
|
+
|
391
|
+
if (flameData && flameData.children && flameData.children.length > 0) {
|
392
|
+
d3.select("#flamegraph-chart")
|
393
|
+
.datum(flameData)
|
394
|
+
.call(flameChart);
|
395
|
+
} else {
|
396
|
+
flamegraphContainer.textContent = 'No profiling data captured for flame graph.';
|
397
|
+
}
|
398
|
+
|
399
|
+
window.addEventListener('resize', () => {
|
400
|
+
d3.select("#flamegraph-chart").selectAll(":scope > *").remove();
|
401
|
+
|
402
|
+
// Update width and height on resize
|
403
|
+
const newWidth = flamegraphContainer.clientWidth || 960;
|
404
|
+
const newHeight = flamegraphContainer.clientHeight || 400;
|
405
|
+
flameChart.width(newWidth).height(newHeight);
|
406
|
+
|
407
|
+
d3.select("#flamegraph-chart")
|
408
|
+
.datum(flameData)
|
409
|
+
.call(flameChart);
|
410
|
+
});
|
411
|
+
|
412
|
+
const barChartContainer = document.getElementById('barchart');
|
413
|
+
const maxTotalTime = chartData.length > 0 ? Math.max(...chartData.map(d => d.total)) : 1;
|
414
|
+
|
415
|
+
chartData.forEach(d => {
|
416
|
+
const row = document.createElement('div');
|
417
|
+
row.className = 'bar-row';
|
418
|
+
const label = document.createElement('div');
|
419
|
+
label.className = 'bar-label';
|
420
|
+
label.textContent = d.name;
|
421
|
+
label.title = d.name;
|
422
|
+
// Add style if internal flag (propagated via barChartData) is true
|
423
|
+
if (d.internal) {
|
424
|
+
label.style.color = 'var(--accent-light)';
|
425
|
+
}
|
426
|
+
|
427
|
+
const barBg = document.createElement('div');
|
428
|
+
barBg.className = 'bar-rect-bg';
|
429
|
+
const bar = document.createElement('div');
|
430
|
+
bar.className = 'bar-rect';
|
431
|
+
const barWidthPercent = (d.total / maxTotalTime) * 100;
|
432
|
+
bar.style.width = barWidthPercent.toFixed(1) + '%';
|
433
|
+
const value = document.createElement('span');
|
434
|
+
value.className = 'bar-value';
|
435
|
+
value.textContent = d.total.toFixed(2) + 'ms';
|
436
|
+
bar.appendChild(value);
|
437
|
+
const statsText = \`avg: \${d.avg.toFixed(2)}ms | min: \${d.min.toFixed(2)}ms | max: \${d.max.toFixed(2)}ms | count: \${d.count}\`;
|
438
|
+
const statsInline = document.createElement('div');
|
439
|
+
statsInline.className = 'bar-stats-inline';
|
440
|
+
statsInline.textContent = statsText;
|
441
|
+
barBg.appendChild(bar);
|
442
|
+
barBg.appendChild(statsInline);
|
443
|
+
row.appendChild(label);
|
444
|
+
row.appendChild(barBg);
|
445
|
+
barChartContainer.appendChild(row);
|
446
|
+
});
|
447
|
+
|
448
|
+
if (chartData.length === 0) {
|
449
|
+
barChartContainer.textContent = 'No profiling data captured for bar chart.';
|
450
|
+
}
|
451
|
+
</script>
|
452
|
+
</body>
|
453
|
+
</html>
|
454
|
+
`;
|
455
|
+
}
|
456
|
+
|
457
|
+
// --- HTTP Server (remains the same) ---
|
458
|
+
const html = generateHtml(d3FlameGraphData, barChartData);
|
459
|
+
|
460
|
+
const port = 8080;
|
461
|
+
const server = http.createServer((req, res) => {
|
462
|
+
if (req.url === '/' || req.url === '/index.html') {
|
463
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
464
|
+
res.end(html);
|
465
|
+
} else if (req.url === '/favicon.ico') {
|
466
|
+
res.writeHead(204, { 'Content-Type': 'image/x-icon' }); // No content for favicon
|
467
|
+
res.end();
|
468
|
+
} else {
|
469
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
470
|
+
res.end('Not Found');
|
471
|
+
}
|
472
|
+
});
|
473
|
+
|
474
|
+
server.listen(port, () => {
|
475
|
+
console.log(`\nProfile report available at: http://localhost:${port}`);
|
476
|
+
console.log('Press Ctrl+C to stop the server.');
|
477
|
+
});
|
478
|
+
|
479
|
+
server.on('error', (err) => {
|
480
|
+
if (err.code === 'EADDRINUSE') {
|
481
|
+
console.error(`Error: Port ${port} is already in use. Please stop the other process or choose a different port.`);
|
482
|
+
} else {
|
483
|
+
console.error('Server error:', err);
|
484
|
+
}
|
485
|
+
process.exit(1);
|
486
|
+
});
|
package/runtime/hotlines.js
CHANGED
@@ -18,10 +18,10 @@ const spinner = ['-', '\\', '|', '/'];
|
|
18
18
|
let spin = 0;
|
19
19
|
let last = 0;
|
20
20
|
|
21
|
-
createImport('profile1',
|
21
|
+
createImport('profile1', [ Valtype.i32 ], 0, n => {
|
22
22
|
tmp[n] = performance.now();
|
23
23
|
});
|
24
|
-
createImport('profile2',
|
24
|
+
createImport('profile2', [ Valtype.i32 ], 0, n => {
|
25
25
|
const t = performance.now();
|
26
26
|
times[n] += t - tmp[n];
|
27
27
|
|
@@ -30,7 +30,7 @@ createImport('profile2', 1, 0, n => {
|
|
30
30
|
process.stdout.write(`\r${spinner[spin++ % 4]} running: collected ${samples} samples...`);
|
31
31
|
last = t + 100;
|
32
32
|
}
|
33
|
-
})
|
33
|
+
});
|
34
34
|
|
35
35
|
try {
|
36
36
|
const { exports } = compile(source, undefined);
|