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.
@@ -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
- const termWidth = process.stdout.columns || 80;
16
- const termHeight = process.stdout.rows || 24;
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 spin = 0;
29
+ let spinIdx = 0;
30
+ let lastProgressUpdate = 0;
31
+ const progressUpdateIntervalMs = 500; // Update every 500ms
28
32
 
29
- const onExit = () => {
30
- process.stdout.write('\x1b[1;1H\x1b[J');
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
- const realEnd = end ?? now;
49
- const total = realEnd - start;
50
- const _total = total;
51
- text += `${total.toFixed(0)}ms`;
52
-
53
- const samples = samplesFunc.length;
54
- text += `${' '.repeat(12 - text.length)}┃ samples: ${samples}`;
55
- text += `${' '.repeat(32 - text.length)}┃ render: ${lastRenderTime.toFixed(2)}ms`;
56
-
57
- if (end != null || Prefs.live) {
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
- createImport('profile2', 1, 0, f => { // post-call
53
+
54
+ createImport('profile2', 0, 0, () => { // post-call
156
55
  const now = performance.now();
157
- samplesEnd[running[--runningIdx]] = now;
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 (now > last) {
160
- last = now + 500;
161
- render();
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
- Prefs.treeshakeWasmImports = false;
166
- let funcLookup = new Map();
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
- let local;
178
- if (x.locals['#profile_tmp']) {
179
- local = x.locals['#profile_tmp'].idx;
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 += 4;
95
+ i += 3; // Skip the 3 instructions we just added
188
96
  }
189
97
  }
190
98
  }
191
99
  };
192
100
 
193
- let last = 0;
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
- exports.main();
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
- render();
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
+ });
@@ -18,10 +18,10 @@ const spinner = ['-', '\\', '|', '/'];
18
18
  let spin = 0;
19
19
  let last = 0;
20
20
 
21
- createImport('profile1', 1, 0, n => {
21
+ createImport('profile1', [ Valtype.i32 ], 0, n => {
22
22
  tmp[n] = performance.now();
23
23
  });
24
- createImport('profile2', 1, 0, n => {
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);
package/runtime/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
- globalThis.version = '0.57.14';
3
+ globalThis.version = '0.57.16';
4
4
 
5
5
  // deno compat
6
6
  if (typeof process === 'undefined' && typeof Deno !== 'undefined') {