Skip to content

Commit bfc6c38

Browse files
committed
chore: cleaned up webviz
1 parent 8269547 commit bfc6c38

File tree

5 files changed

+2386
-1323
lines changed

5 files changed

+2386
-1323
lines changed

index.html

Lines changed: 165 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,139 @@
55
<title>Span Visualiser</title>
66
<meta name="viewport" content="width=device-width,initial-scale=1" />
77
<script type="module">
8-
import * as d3 from 'https://cdn.skypack.dev/d3@7';
98
import { bases } from 'https://cdn.skypack.dev/multiformats/basics';
109

11-
//— multibase → timestamp utilities (same as your snippet) —
12-
const unixtsSize = 36,
13-
msecSize = 12,
14-
msecPrecision = 3,
15-
indentPx = 1000;
16-
const basesByPrefix = {};
17-
for (const c of Object.values(bases)) basesByPrefix[c.prefix] = c;
10+
/**
11+
* @typedef {{
12+
* name: string,
13+
* prefix: string,
14+
* encode: (input: Uint8Array) => string,
15+
* decode: (input: string) => Uint8Array
16+
* }} Codec
17+
*/
18+
19+
const unixtsSize = 36;
20+
const msecSize = 12;
21+
const msecPrecision = 3;
22+
23+
/**
24+
* Encodes Uint8Array into bit string
25+
* @param {Uint8Array} bytes
26+
* @return {string}
27+
*/
1828
function bytes2bits(bytes) {
19-
return Array.from(bytes)
20-
.map((b) => b.toString(2).padStart(8, '0'))
21-
.join('');
29+
return [...bytes].map((n) => dec2bits(n, 8)).join('');
2230
}
23-
function fromMultibase(str) {
24-
const codec = basesByPrefix[str[0]];
25-
if (!codec) return;
31+
32+
/**
33+
* Encodes positive base 10 numbers to bit string
34+
* Will output bits in big-endian order
35+
* @param {number} dec
36+
* @param {number|undefined} size
37+
* @return {string}
38+
*/
39+
function dec2bits(dec, size) {
40+
if (dec < 0) throw RangeError('`dec` must be positive');
41+
if (size != null) {
42+
if (size < 0) throw RangeError('`size` must be positive');
43+
if (size === 0) return '';
44+
dec %= 2 ** size;
45+
} else {
46+
size = 0;
47+
}
48+
return dec.toString(2).padStart(size, '0');
49+
}
50+
51+
/** @type {Record<string,Codec>} */
52+
const basesByPrefix = {};
53+
for (const k in bases) {
54+
/** @type {Codec} */
55+
const codec = bases[k];
56+
basesByPrefix[codec.prefix] = codec;
57+
}
58+
59+
/**
60+
* Decodes a multibase encoded ID
61+
* Do not use this for generic multibase strings
62+
* @param {string} idString
63+
* @return {Uint8Array|undefined}
64+
*/
65+
function fromMultibase(idString) {
66+
const prefix = idString[0];
67+
const codec = basesByPrefix[prefix];
68+
if (codec == null) {
69+
return;
70+
}
71+
/** @type {Uint8Array} */
72+
let buffer;
2673
try {
27-
return codec.decode(str);
28-
} catch {
74+
buffer = codec.decode(idString);
75+
} catch (e) {
2976
return;
3077
}
78+
return buffer;
3179
}
32-
function extractTs(idStr) {
33-
const bytes = fromMultibase(idStr);
34-
if (!bytes) return 0;
35-
const tsBytes = bytes.subarray(0, (unixtsSize + msecSize) / 8);
36-
const bits = bytes2bits(tsBytes);
37-
const u = parseInt(bits.slice(0, unixtsSize), 2);
38-
const m = parseInt(bits.slice(unixtsSize, unixtsSize + msecSize), 2);
39-
return u * 1000 + (m / 2 ** msecSize) * 1000;
80+
81+
/**
82+
* Converts fixed point tuple to floating point number
83+
* Size is number of bits allocated for the fractional
84+
* Precision dictates a fixed number of decimal places for the fractional
85+
* @param {[number,number]} [integer, fractionalFixed]
86+
* @param {number} size
87+
* @param {number|undefined} precision
88+
* @return number
89+
*/
90+
function fromFixedPoint([integer, fractionalFixed], size, precision) {
91+
/** @type {number} */
92+
let fractional;
93+
if (precision == null) {
94+
fractional = fractionalFixed / 2 ** size;
95+
} else {
96+
fractional = roundPrecise(fractionalFixed / 2 ** size, precision);
97+
}
98+
return integer + fractional;
4099
}
41100

42-
//— load JSONL either via fetch or via file input fallback —
101+
/**
102+
* Round to number of decimal points
103+
* @param {number} num
104+
* @param {number} digits
105+
* @param {number} base
106+
* @return {number}
107+
*/
108+
function roundPrecise(num, digits = 0, base = 10) {
109+
const pow = Math.pow(base, digits);
110+
return Math.round((num + Number.EPSILON) * pow) / pow;
111+
}
112+
113+
/**
114+
* @param {Uint8Array} idBytes
115+
* @return {number}
116+
*/
117+
function extractTs(idBytes) {
118+
// Decode the timestamp from the last id
119+
// the timestamp bits is 48 bits or 6 bytes
120+
// this creates a new zero-copy view
121+
const idTsBytes = idBytes.subarray(0, (unixtsSize + msecSize) / 8);
122+
const idTsBits = bytes2bits(idTsBytes);
123+
const unixtsBits = idTsBits.substr(0, unixtsSize);
124+
const msecBits = idTsBits.substr(unixtsSize, unixtsSize + msecSize);
125+
const unixts = parseInt(unixtsBits, 2);
126+
const msec = parseInt(msecBits, 2);
127+
// Converting from second and subseconds
128+
return fromFixedPoint([unixts, msec], msecSize, msecPrecision);
129+
}
130+
131+
globalThis.idUtils = {
132+
fromMultibase,
133+
extractTs,
134+
};
135+
</script>
136+
<script type="module">
137+
/**
138+
* Loads JSONL by asking the user to upload a file
139+
* @return {Promise<Array<string>>} Array of lines
140+
*/
43141
function loadLines() {
44142
return new Promise((resolve) => {
45143
const input = document.createElement('input');
@@ -59,52 +157,61 @@
59157
});
60158
}
61159

62-
//— main —
63-
(async () => {
64-
const rawLines = await loadLines();
160+
globalThis.utils = {
161+
loadLines,
162+
};
163+
</script>
164+
<script type="module">
165+
import * as d3 from 'https://cdn.skypack.dev/d3@7';
166+
167+
const main = async () => {
168+
const rawLines = await utils.loadLines();
65169
const events = rawLines.map((l) => JSON.parse(l));
66170

67-
// 1) group by spanId
68-
const evBySpan = new Map();
171+
// 1) Group by spanId
172+
const eventBySpan = new Map();
69173
for (const e of events) {
70-
const sid = e.type === 'start' ? e.id : e.startId;
71-
if (!evBySpan.has(sid)) evBySpan.set(sid, []);
72-
evBySpan.get(sid).push(e);
174+
const spanId = e.type === 'start' ? e.id : e.startId;
175+
if (!eventBySpan.has(spanId)) eventBySpan.set(spanId, []);
176+
eventBySpan.get(spanId).push(e);
73177
}
74178

75-
// 2) for each span, sort events, pick start/end, record parent
179+
// 2) For each span, sort events, pick start/end, record parent
76180
const spanInfo = {};
77-
for (const [sid, evts] of evBySpan.entries()) {
181+
for (const [sid, evts] of eventBySpan.entries()) {
78182
evts.sort(
79183
(a, b) =>
80-
extractTs(a.id || a.startId) - extractTs(b.id || b.startId),
184+
idUtils.extractTs(idUtils.fromMultibase(a.id || a.startId)) -
185+
idUtils.extractTs(idUtils.fromMultibase(b.id || b.startId)),
81186
);
82187
const startEvt = evts.find((e) => e.type === 'start');
83-
const stopEvt = evts.find(
84-
(e) => e.type === 'stop' || e.type === 'end',
85-
);
188+
const stopEvt = evts.find((e) => e.type === 'stop');
86189
spanInfo[sid] = {
87190
id: sid,
88191
name: startEvt?.name || sid,
89-
ts0: startEvt ? extractTs(startEvt.id) : Infinity,
90-
ts1: stopEvt ? extractTs(stopEvt.id) : -Infinity,
192+
ts0: startEvt
193+
? idUtils.extractTs(idUtils.fromMultibase(startEvt.id)) * 1000
194+
: Infinity,
195+
ts1: stopEvt
196+
? idUtils.extractTs(idUtils.fromMultibase(stopEvt.id)) * 1000
197+
: -Infinity,
91198
parent: startEvt?.parentSpanId || startEvt?.parentId || null,
92199
hasEnd: !!stopEvt,
93200
};
94201
}
95202

96-
// 3) fix open spans → draw to maxTime
97-
const allStarts = Object.values(spanInfo).map((d) => d.ts0);
203+
// 2.1) [optional] Fix open spans by drawing to maxTime
204+
const allStarts = Object.values(spanInfo).map((e) => e.ts0);
98205
const allEnds = Object.values(spanInfo)
99-
.map((d) => d.ts1)
100-
.filter((d) => d > 0);
206+
.map((e) => e.ts1)
207+
.filter((t) => t > 0);
101208
const minT = Math.min(...allStarts);
102209
const maxT = Math.max(...allEnds);
103210
for (const s of Object.values(spanInfo)) {
104211
if (s.ts1 < 0) s.ts1 = maxT;
105212
}
106213

107-
// 4) build tree & compute depths
214+
// 3) Build tree & compute depths
108215
const children = {};
109216
for (const sid of Object.keys(spanInfo)) children[sid] = [];
110217
for (const s of Object.values(spanInfo)) {
@@ -170,31 +277,17 @@
170277
yPos += verticalSpacing;
171278

172279
if (node.children) {
173-
node.children.forEach(child => layoutTree(child, depth + 1));
280+
node.children.forEach((child) => layoutTree(child, depth + 1));
174281
}
175282
}
176-
rootNodes.forEach(node => layoutTree(node));
283+
rootNodes.forEach((node) => layoutTree(node));
177284

178285
// 7) Set up scales
179-
const x = d3.scaleLinear()
286+
const x = d3
287+
.scaleLinear()
180288
.domain([minT, maxT])
181289
.range([margin.left, W - margin.right]);
182290

183-
// arrowhead
184-
svg
185-
.append('defs')
186-
.append('marker')
187-
.attr('id', 'arrowHead')
188-
.attr('viewBox', '-5 -5 10 10')
189-
.attr('refX', 0)
190-
.attr('refY', 0)
191-
.attr('markerWidth', 6)
192-
.attr('markerHeight', 6)
193-
.attr('orient', 'auto')
194-
.append('path')
195-
.attr('d', 'M-5,-5L5,0L-5,5')
196-
.attr('fill', '#339');
197-
198291
// pan/zoom
199292
const g = svg.append('g');
200293
svg.call(d3.zoom().on('zoom', (e) => g.attr('transform', e.transform)));
@@ -206,20 +299,19 @@
206299
if (node.parent && nodeMap.has(node.parent)) {
207300
const parentY = nodePositions.get(node.parent);
208301
const childY = nodePositions.get(node.id);
209-
g.append('line')
302+
g.append('line')
210303
.attr('x1', x(node.ts0))
211304
.attr('y1', parentY)
212305
.attr('x2', x(node.ts0))
213306
.attr('y2', childY)
214-
.attr('stroke', '#ccc')
215-
.attr('stroke-width', 1);
307+
.attr('stroke', '#aaa')
308+
.attr('stroke-width', 2);
216309
}
217310
});
218311

219312
// Draw span bars on top of connections
220313
nodeMap.forEach((node) => {
221314
const yPos = nodePositions.get(node.id);
222-
// console.log(node);
223315

224316
// Timeline bar
225317
g.append('line')
@@ -228,8 +320,7 @@
228320
.attr('x2', x(node.ts1))
229321
.attr('y2', yPos)
230322
.attr('stroke', node.hasEnd ? '#258' : '#911')
231-
.attr('stroke-width', 4)
232-
// .attr('stroke-dasharray', node.hasEnd ? null : "4,4");
323+
.attr('stroke-width', 4);
233324

234325
// Label
235326
g.append('text')
@@ -242,15 +333,17 @@
242333

243334
const xAxis = d3
244335
.axisBottom(x)
245-
.ticks(10)
336+
.ticks(20)
246337
.tickFormat((d) => new Date(d).toISOString().slice(11, 23));
247338

248339
g.append('g')
249340
.attr('transform', `translate(0,${margin.top - 48})`)
250341
.call(xAxis)
251342
.selectAll('path, line')
252343
.attr('stroke', '#666');
253-
})();
344+
};
345+
346+
main();
254347
</script>
255348
<style>
256349
body {
@@ -264,7 +357,6 @@
264357
</style>
265358
</head>
266359
<body>
267-
<!-- if fetch fails, a file-picker will be injected by the script -->
268360
<svg></svg>
269361
</body>
270362
</html>

npmDepsHash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sha256-sUrFNZdpwjBXOaIK+skCBJ8M3zgQqdQrby1kvSQ90X4=
1+
sha256-yOUsAbtVeXMdGjR92SvSCFAlPTHxm1xQCipqbdkxb+o=

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@
137137
},
138138
"devDependencies": {
139139
"@matrixai/errors": "^2.1.3",
140-
"@matrixai/logger": "^4.0.4-alpha.4",
140+
"@matrixai/logger": "^4.0.4-alpha.5",
141141
"@matrixai/exec": "^1.0.3",
142142
"@fast-check/jest": "^2.1.1",
143143
"@swc/core": "1.3.82",
@@ -173,6 +173,6 @@
173173
"typescript": "^5.1.6"
174174
},
175175
"overrides": {
176-
"@matrixai/logger": "^4.0.4-alpha.4"
176+
"@matrixai/logger": "^4.0.4-alpha.5"
177177
}
178178
}

0 commit comments

Comments
 (0)