porffor 0.57.16 → 0.57.17

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/README.md CHANGED
@@ -40,9 +40,9 @@ Expect nothing to work! Only very limited JS is currently supported. See files i
40
40
 
41
41
  ### Profiling a JS file
42
42
  > [!WARNING]
43
- > Very experimental WIP feature!
43
+ > Experimental WIP feature!
44
44
 
45
- **`porf hotlines path/to/script.js`**
45
+ **`porf profile path/to/script.js`**
46
46
 
47
47
  ### Debugging a JS file
48
48
  > [!WARNING]
package/foo.js CHANGED
@@ -1,26 +1 @@
1
- function buildString(args) {
2
- // Use member expressions rather than destructuring `args` for improved
3
- // compatibility with engines that only implement assignment patterns
4
- // partially or not at all.
5
- const loneCodePoints = args.loneCodePoints;
6
- const ranges = args.ranges;
7
- const CHUNK_SIZE = 10000;
8
- let result = String.fromCodePoint.apply(null, loneCodePoints);
9
- for (let i = 0; i < ranges.length; i++) {
10
- let range = ranges[i];
11
- let start = range[0];
12
- let end = range[1];
13
- let codePoints = [];
14
- for (let length = 0, codePoint = start; codePoint <= end; codePoint++) {
15
- codePoints[length++] = codePoint;
16
- if (length === CHUNK_SIZE) {
17
- result += String.fromCodePoint.apply(null, codePoints);
18
- codePoints.length = length = 0;
19
- }
20
- }
21
- result += String.fromCodePoint.apply(null, codePoints);
22
- }
23
- return result;
24
- }
25
-
26
- console.log(buildString({ loneCodePoints: [1337], ranges: [] }));
1
+ console.log(process.argv);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "porffor",
3
3
  "description": "An ahead-of-time JavaScript compiler",
4
- "version": "0.57.16",
4
+ "version": "0.57.17",
5
5
  "author": "Oliver Medhurst <honk@goose.icu>",
6
6
  "license": "MIT",
7
7
  "scripts": {},
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.16';
3
+ globalThis.version = '0.57.17';
4
4
 
5
5
  // deno compat
6
6
  if (typeof process === 'undefined' && typeof Deno !== 'undefined') {
@@ -24,11 +24,8 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
24
24
  c: [ 94, 'foo.js foo.c', 'Compile to C source code' ],
25
25
  native: [ 94, 'foo.js foo', 'Compile to a native binary' ],
26
26
 
27
- 'Profile': [],
28
- flamegraph: [ 93, 'foo.js', 'View detailed func-by-func performance' ],
29
- hotlines: [ 93, 'foo.js', 'View source with line-by-line performance' ],
30
-
31
- 'Debug': [],
27
+ 'Analyze': [],
28
+ profile: [ 93, 'foo.js', 'View detailed func-by-func performance' ],
32
29
  debug: [ 33, 'foo.js', 'Debug the source of a file' ],
33
30
  dissect: [ 33, 'foo.js', 'Debug the compiled Wasm of a file' ],
34
31
  })) {
@@ -93,7 +90,7 @@ const done = async () => {
93
90
  };
94
91
 
95
92
  let file = process.argv.slice(2).find(x => x[0] !== '-');
96
- if (['precompile', 'run', 'wasm', 'native', 'c', 'flamegraph', 'hotlines', 'debug', 'dissect'].includes(file)) {
93
+ if (['precompile', 'run', 'wasm', 'native', 'c', 'profile', 'debug', 'dissect'].includes(file)) {
97
94
  // remove this arg
98
95
  process.argv.splice(process.argv.indexOf(file), 1);
99
96
 
@@ -102,13 +99,8 @@ if (['precompile', 'run', 'wasm', 'native', 'c', 'flamegraph', 'hotlines', 'debu
102
99
  await done();
103
100
  }
104
101
 
105
- if (file === 'flamegraph') {
106
- await import('./flamegraph.js');
107
- await done();
108
- }
109
-
110
- if (file === 'hotlines') {
111
- await import('./hotlines.js');
102
+ if (file === 'profile') {
103
+ await import('./profile.js');
112
104
  await done();
113
105
  }
114
106
 
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ import { Opcodes, Valtype } from '../compiler/wasmSpec.js';
3
+ import { number } from '../compiler/encoding.js';
4
+ import { importedFuncs } from '../compiler/builtins.js';
5
+ import compile, { createImport } from '../compiler/wrap.js';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ const file = process.argv.slice(2).find(x => x[0] !== '-');
10
+
11
+ let host = globalThis?.navigator?.userAgent;
12
+ if (typeof process !== 'undefined' && process.argv0 === 'node') host = 'Node/' + process.versions.node;
13
+ host ??= 'Unknown';
14
+
15
+ const title = process.argv.slice(process.argv.indexOf(file) + 1).find(x => x[0] !== '-') ?? path.basename(file);
16
+
17
+ let source = fs.readFileSync(file, 'utf8');
18
+
19
+ const samplesFunc = []; // Stores function index for each sample
20
+ const samplesStart = []; // Stores start timestamp for each sample
21
+ const samplesEnd = []; // Stores end timestamp for each sample (filled in profile2)
22
+ let funcLookup = new Map(); // Maps function index to { index, name, internal, ... }
23
+ let start, end; // Overall start and end time of execution
24
+
25
+ // --- Performance Tuning ---
26
+ const minSampleDurationMs = 0.01; // Ignore samples shorter than this for flame graph hierarchy
27
+
28
+ // --- Spinner Globals ---
29
+ const spinner = ['-', '\\', '|', '/'];
30
+ let spinIdx = 0;
31
+ let lastProgressUpdate = 0;
32
+ const progressUpdateIntervalMs = 500; // Update every 500ms
33
+
34
+ // Stack tracking for flamegraph construction
35
+ let running = new Uint32Array(1024); // Stores indices into samplesFunc/Start/End arrays
36
+ let runningIdx = 0;
37
+
38
+ // --- Profiling Hooks ---
39
+ createImport('profile1', [ Valtype.i32 ], 0, f => { // pre-call
40
+ const now = performance.now();
41
+ samplesStart.push(now);
42
+ // Store the *index* of the just pushed start time/func id
43
+ // This index corresponds to the entry in samplesFunc/Start/End
44
+ const sampleIndex = samplesFunc.push(f) - 1;
45
+ samplesEnd.push(null); // Placeholder for end time
46
+ if (runningIdx >= running.length) {
47
+ // Resize running buffer if needed
48
+ const newRunning = new Uint32Array(running.length * 2);
49
+ newRunning.set(running);
50
+ running = newRunning;
51
+ }
52
+ running[runningIdx++] = sampleIndex;
53
+ });
54
+
55
+ createImport('profile2', 0, 0, () => { // post-call
56
+ const now = performance.now();
57
+ if (runningIdx > 0) {
58
+ const sampleIndex = running[--runningIdx];
59
+ // Only set end time if it hasn't been set (handles potential async overlaps?)
60
+ if (samplesEnd[sampleIndex] === null) {
61
+ samplesEnd[sampleIndex] = now;
62
+ }
63
+ }
64
+
65
+ // Check if it's time to update progress spinner
66
+ if (now - lastProgressUpdate > progressUpdateIntervalMs) {
67
+ lastProgressUpdate = now;
68
+ const sampleCount = samplesFunc.length;
69
+ const currentSpinner = spinner[spinIdx++ % spinner.length];
70
+ const termWidth = process.stdout.columns || 80;
71
+ const output = `\r${currentSpinner} Running... Samples: ${sampleCount}`;
72
+ // Write progress, ensuring the rest of the line is cleared
73
+ process.stdout.write(output + ' '.repeat(Math.max(0, termWidth - output.length - 1)));
74
+ }
75
+ });
76
+
77
+ // --- Compilation ---
78
+ Prefs.treeshakeWasmImports = false; // Keep profile imports
79
+ globalThis.compileCallback = ({ funcs }) => {
80
+ funcLookup = new Map(); // Reset map
81
+ for (const x of funcs) {
82
+ funcLookup.set(x.index, x);
83
+
84
+ // Inject profiling calls around existing calls
85
+ const w = x.wasm;
86
+ for (let i = 0; i < w.length; i++) {
87
+ if (w[i][0] === Opcodes.call) {
88
+ const f = w[i][1];
89
+ // Don't profile calls to imported funcs (like profile1/2 itself)
90
+ if (f < importedFuncs.length) continue;
91
+
92
+ // Inject profile2 *after* the call
93
+ w.splice(i + 1, 0, [ Opcodes.call, importedFuncs.profile2 ]);
94
+ // Inject function index push and profile1 *before* the call
95
+ w.splice(i, 0, number(f, Valtype.i32), [ Opcodes.call, importedFuncs.profile1 ]);
96
+ i += 3; // Skip the 3 instructions we just added
97
+ }
98
+ }
99
+ }
100
+ };
101
+
102
+ console.log('Compiling...');
103
+ const { exports } = compile(source, undefined, {}, () => {});
104
+
105
+ // --- Execution with Progress Spinner ---
106
+
107
+ console.log('Starting execution...');
108
+ // Initial placeholder message
109
+ const initialMsg = "Running... (waiting for first samples)";
110
+ const termWidthInitial = process.stdout.columns || 80;
111
+ process.stdout.write('\r' + initialMsg + ' '.repeat(Math.max(0, termWidthInitial - initialMsg.length -1)));
112
+
113
+ start = performance.now();
114
+
115
+ try {
116
+ exports.main();
117
+ } finally {
118
+ // Clear the spinner line
119
+ const termWidthFinal = process.stdout.columns || 80;
120
+ process.stdout.write('\r' + ' '.repeat(termWidthFinal) + '\r');
121
+ }
122
+
123
+ end = performance.now();
124
+ console.log(`Execution finished in ${(end - start).toFixed(2)}ms`);
125
+
126
+ // --- Data Processing ---
127
+
128
+ const totalDuration = end - start;
129
+ // const processedSamples = []; // Remove: We'll create a filtered list directly
130
+ const filteredSamples = []; // Samples that meet the duration threshold for the flame graph
131
+ const funcStats = new Map(); // { index -> { total: 0, count: 0, min: Infinity, max: -Infinity, name: '', internal: false }}
132
+
133
+ // Process raw samples: Calculate stats for all, filter for flame graph
134
+ console.log(`Processing ${samplesFunc.length} raw samples...`);
135
+ let samplesBelowThreshold = 0;
136
+ for (let i = 0; i < samplesFunc.length; i++) {
137
+ const funcIndex = samplesFunc[i];
138
+ const func = funcLookup.get(funcIndex);
139
+ const funcName = func ? func.name : `unknown_${funcIndex}`;
140
+ const isInternal = func ? !!func.internal : false; // Read internal flag
141
+ const startTime = samplesStart[i];
142
+ const endTime = samplesEnd[i] === null ? end : samplesEnd[i]; // Cap duration
143
+ const duration = endTime - startTime;
144
+
145
+ if (duration < 0) continue; // Skip potentially erroneous samples
146
+
147
+ // --- Update function stats (always do this) ---
148
+ if (!funcStats.has(funcIndex)) {
149
+ funcStats.set(funcIndex, { total: 0, count: 0, min: Infinity, max: -Infinity, name: funcName, internal: isInternal }); // Store internal flag
150
+ }
151
+ const stats = funcStats.get(funcIndex);
152
+ stats.total += duration;
153
+ stats.count++;
154
+ if (duration < stats.min) stats.min = duration;
155
+ if (duration > stats.max) stats.max = duration;
156
+
157
+ // --- Filter samples for flame graph hierarchy ---
158
+ if (duration >= minSampleDurationMs) {
159
+ filteredSamples.push({
160
+ // Only store data needed for buildHierarchy
161
+ name: funcName,
162
+ start: startTime - start, // Relative to overall start
163
+ end: endTime - start, // Relative to overall start
164
+ duration: duration,
165
+ internal: isInternal // Store internal flag for flamegraph nodes
166
+ });
167
+ } else {
168
+ samplesBelowThreshold++;
169
+ }
170
+ }
171
+ console.log(`Filtered out ${samplesBelowThreshold} samples shorter than ${minSampleDurationMs}ms.`);
172
+ console.log(`Building hierarchy from ${filteredSamples.length} samples...`);
173
+
174
+ // --- d3-flame-graph Data Generation ---
175
+ // Requires a hierarchical structure: { name: 'root', value: total, children: [...] }
176
+ // where value represents the total time (inclusive of children)
177
+ function buildHierarchy(samples) {
178
+ if (!samples || samples.length === 0) {
179
+ return { name: path.basename(file), value: 0, children: [], internal: false }; // Root is not internal
180
+ }
181
+
182
+ // Sort primarily by start time, secondarily by end time descending (parents first)
183
+ samples.sort((a, b) => a.start - b.start || b.end - a.end);
184
+
185
+ const root = { name: path.basename(file), value: 0, children: [], internal: false }; // Root is not internal
186
+ root.startTime = 0;
187
+ root.endTime = totalDuration;
188
+ const stack = [{ node: root, startTime: root.startTime, endTime: root.endTime }]; // Consistent structure
189
+
190
+ for (const sample of samples) {
191
+ // Pass internal flag from filteredSample to newNode
192
+ const newNode = { name: sample.name, value: sample.duration, children: [], internal: sample.internal };
193
+ const sampleStartTime = sample.start;
194
+ const sampleEndTime = sample.end;
195
+
196
+ // Pop stack until parent is found
197
+ // Parent must start before or at the same time, and end after or at the same time
198
+ // Accessing .startTime and .endTime on stack elements is correct here
199
+ while (stack.length > 1 && (stack[stack.length - 1].startTime > sampleStartTime || stack[stack.length - 1].endTime < sampleEndTime)) {
200
+ stack.pop();
201
+ }
202
+
203
+ const parent = stack[stack.length - 1];
204
+ parent.node.children.push(newNode); // Correctly access children via parent.node
205
+
206
+ // Add node to stack with its *graph node* reference and end time
207
+ stack.push({ node: newNode, startTime: sampleStartTime, endTime: sampleEndTime });
208
+ }
209
+
210
+ // d3-flamegraph expects `value` to be inclusive time, but we provide durations.
211
+ // The library handles the aggregation based on children, so we just pass the duration.
212
+ // Let's ensure root value is the total duration if samples don't cover it
213
+ // root.value = Math.max(root.value, samples.reduce((sum, s) => sum + s.duration, 0)); // Remove: Not needed with selfValue(true)
214
+
215
+ return root;
216
+ }
217
+
218
+ const d3FlameGraphData = buildHierarchy(filteredSamples);
219
+
220
+ // --- Bar Chart Data Generation (remains the same) ---
221
+ const barChartData = Array.from(funcStats.values())
222
+ .map(stats => ({
223
+ name: stats.name,
224
+ total: stats.total,
225
+ min: stats.min === Infinity ? 0 : stats.min,
226
+ max: stats.max === -Infinity ? 0 : stats.max,
227
+ avg: stats.count > 0 ? stats.total / stats.count : 0,
228
+ count: stats.count,
229
+ internal: stats.internal // Include internal flag from funcStats
230
+ }))
231
+ .sort((a, b) => b.total - a.total) // Sort by total time descending
232
+ .slice(0, 50); // Limit to top 50 functions
233
+
234
+ console.log('uploading...');
235
+ const { id } = await (await fetch('https://profile.porffor.dev', {
236
+ method: 'POST',
237
+ headers: {
238
+ 'Content-Type': 'application/json'
239
+ },
240
+ body: JSON.stringify({
241
+ flame: d3FlameGraphData,
242
+ chart: barChartData,
243
+ title,
244
+ subtitle: `Porffor ${globalThis.version} on ${host.replace('/', ' ')} | ${new Date().toISOString().slice(0, -8).replace('T', ' ')}\n${totalDuration.toFixed(2)}ms | ${samplesFunc.length} samples`
245
+ })
246
+ })).json();
247
+ console.log(`https://profile.porffor.dev/${id}`);
248
+ process.exit(0);
@@ -1,486 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Opcodes, Valtype } from '../compiler/wasmSpec.js';
3
- import { number } from '../compiler/encoding.js';
4
- import { importedFuncs } from '../compiler/builtins.js';
5
- import compile, { createImport } from '../compiler/wrap.js';
6
- import fs from 'node:fs';
7
- import http from 'node:http';
8
- import path from 'node:path';
9
- import url from 'node:url';
10
-
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
- }
16
- let source = fs.readFileSync(file, 'utf8');
17
-
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
23
-
24
- // --- Performance Tuning ---
25
- const minSampleDurationMs = 0.01; // Ignore samples shorter than this for flame graph hierarchy
26
-
27
- // --- Spinner Globals ---
28
- const spinner = ['-', '\\', '|', '/'];
29
- let spinIdx = 0;
30
- let lastProgressUpdate = 0;
31
- const progressUpdateIntervalMs = 500; // Update every 500ms
32
-
33
- // Stack tracking for flamegraph construction
34
- let running = new Uint32Array(1024); // Stores indices into samplesFunc/Start/End arrays
35
- let runningIdx = 0;
36
-
37
- // --- Profiling Hooks ---
38
- createImport('profile1', [ Valtype.i32 ], 0, f => { // pre-call
39
- const now = performance.now();
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;
50
- }
51
- running[runningIdx++] = sampleIndex;
52
- });
53
-
54
- createImport('profile2', 0, 0, () => { // post-call
55
- const now = performance.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
- }
63
-
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)));
73
- }
74
- });
75
-
76
- // --- Compilation ---
77
- Prefs.treeshakeWasmImports = false; // Keep profile imports
78
- globalThis.compileCallback = ({ funcs }) => {
79
- funcLookup = new Map(); // Reset map
80
- for (const x of funcs) {
81
- funcLookup.set(x.index, x);
82
-
83
- // Inject profiling calls around existing calls
84
- const w = x.wasm;
85
- for (let i = 0; i < w.length; i++) {
86
- if (w[i][0] === Opcodes.call) {
87
- const f = w[i][1];
88
- // Don't profile calls to imported funcs (like profile1/2 itself)
89
- if (f < importedFuncs.length) continue;
90
-
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
94
- w.splice(i, 0, number(f, Valtype.i32), [ Opcodes.call, importedFuncs.profile1 ]);
95
- i += 3; // Skip the 3 instructions we just added
96
- }
97
- }
98
- }
99
- };
100
-
101
- console.log('Compiling...');
102
- const { exports } = compile(source, undefined, {}, () => {});
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
-
112
- start = performance.now();
113
-
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
-
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
204
-
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
- });
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env node
2
- import compile, { createImport } from '../compiler/wrap.js';
3
- import fs from 'node:fs';
4
-
5
- const file = process.argv.slice(2).find(x => x[0] !== '-');
6
- let source = fs.readFileSync(file, 'utf8');
7
-
8
- let profileId = 0;
9
- source = source.replace(/^[^\n}]*;$/mg, _ => `profile1(Porffor.wasm.i32.const(${profileId}));${_}profile2(Porffor.wasm.i32.const(${profileId++}));`)
10
-
11
- let tmp = new Array(profileId).fill(0);
12
- let times = new Array(profileId).fill(0);
13
- let samples = 0;
14
-
15
- const percents = process.argv.includes('-%');
16
-
17
- const spinner = ['-', '\\', '|', '/'];
18
- let spin = 0;
19
- let last = 0;
20
-
21
- createImport('profile1', [ Valtype.i32 ], 0, n => {
22
- tmp[n] = performance.now();
23
- });
24
- createImport('profile2', [ Valtype.i32 ], 0, n => {
25
- const t = performance.now();
26
- times[n] += t - tmp[n];
27
-
28
- samples++;
29
- if (t > last) {
30
- process.stdout.write(`\r${spinner[spin++ % 4]} running: collected ${samples} samples...`);
31
- last = t + 100;
32
- }
33
- });
34
-
35
- try {
36
- const { exports } = compile(source, undefined);
37
- const start = performance.now();
38
- exports.main();
39
-
40
- const total = performance.now() - start;
41
- console.log(`\ntotal: ${total}ms\nsamples: ${samples}\n\n\n` + source.split('\n').map(x => {
42
- let time = 0;
43
- if (x.startsWith('profile')) {
44
- const id = parseInt(x.slice(32, x.indexOf(')')));
45
- time = times[id]
46
- }
47
-
48
- let color = [ 0, 0, 0 ];
49
- if (time) {
50
- let relativeTime = time / total;
51
- if (percents) time = relativeTime;
52
-
53
- relativeTime = Math.sqrt(relativeTime);
54
- color = [ (relativeTime * 250) | 0, (Math.sin(relativeTime * Math.PI) * 50) | 0, 0 ];
55
- }
56
-
57
- const ansiColor = `2;${color[0]};${color[1]};${color[2]}m`;
58
-
59
- const line = x.replace(/profile[0-9]\(Porffor.wasm.i32.const\([0-9]+\)\);/g, '');
60
-
61
- if (percents) return `\x1b[48;${ansiColor}\x1b[97m${time ? ((time * 100).toFixed(0).padStart(4, ' ') + '%') : ' '}\x1b[0m\x1b[38;${ansiColor}▌\x1b[0m ${line}`;
62
-
63
- let digits = 2;
64
- if (time >= 100) digits = 1;
65
- if (time >= 1000) digits = 0;
66
-
67
- return `\x1b[48;${ansiColor}\x1b[97m${time ? time.toFixed(digits).padStart(6, ' ') : ' '}\x1b[0m\x1b[38;${ansiColor}▌\x1b[0m ${line}`;
68
- }).join('\n'));
69
- } catch (e) {
70
- console.error(e);
71
- }