Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tools/svg.py
2723 views
1
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2
3
class Node:
4
def __init__(self):
5
self.name = ""
6
self.children = {}
7
# computed
8
self.depth = 0
9
self.width = 0
10
self.offset = 0
11
12
def child(self, name):
13
node = self.children.get(name)
14
if not node:
15
node = self.__class__()
16
node.name = name
17
self.children[name] = node
18
return node
19
20
def subtree(self):
21
result = [self]
22
offset = 0
23
24
while offset < len(result):
25
p = result[offset]
26
offset += 1
27
for c in p.children.values():
28
result.append(c)
29
30
return result
31
32
def escape(s):
33
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
34
35
def layout(root, widthcb):
36
for n in reversed(root.subtree()):
37
# propagate width to the parent
38
n.width = widthcb(n)
39
for c in n.children.values():
40
n.width += c.width
41
42
# compute offset from parent for every child in width order (layout order)
43
offset = 0
44
for c in sorted(n.children.values(), key = lambda x: x.width, reverse = True):
45
c.offset = offset
46
offset += c.width
47
48
for n in root.subtree():
49
for c in n.children.values():
50
c.depth = n.depth + 1
51
c.offset += n.offset
52
53
# svg template (stolen from framegraph.pl)
54
template = r"""<?xml version="1.0" standalone="no"?>
55
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
56
<svg version="1.1" width="1200" height="$height" onload="init(evt)" viewBox="0 0 1200 $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
57
<!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. -->
58
<defs>
59
<linearGradient id="background" y1="0" y2="1" x1="0" x2="0" >
60
<stop stop-color="$gradient-start" offset="5%" />
61
<stop stop-color="$gradient-end" offset="95%" />
62
</linearGradient>
63
</defs>
64
<style type="text/css">
65
text { font-family:Verdana; font-size:12px; fill:rgb(0,0,0); }
66
#search, #ignorecase { opacity:0.1; cursor:pointer; }
67
#search:hover, #search.show, #ignorecase:hover, #ignorecase.show { opacity:1; }
68
#subtitle { text-anchor:middle; font-color:rgb(160,160,160); }
69
#title { text-anchor:middle; font-size:17px}
70
#unzoom { cursor:pointer; }
71
#frames > *:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
72
.hide { display:none; }
73
.parent { opacity:0.5; }
74
</style>
75
<script type="text/ecmascript">
76
<![CDATA[
77
"use strict";
78
var details, searchbtn, unzoombtn, matchedtxt, svg, searching, currentSearchTerm, ignorecase, ignorecaseBtn;
79
function init(evt) {
80
details = document.getElementById("details").firstChild;
81
searchbtn = document.getElementById("search");
82
ignorecaseBtn = document.getElementById("ignorecase");
83
unzoombtn = document.getElementById("unzoom");
84
matchedtxt = document.getElementById("matched");
85
svg = document.getElementsByTagName("svg")[0];
86
searching = 0;
87
currentSearchTerm = null;
88
}
89
90
window.addEventListener("click", function(e) {
91
var target = find_group(e.target);
92
if (target) {
93
if (target.nodeName == "a") {
94
if (e.ctrlKey === false) return;
95
e.preventDefault();
96
}
97
if (target.classList.contains("parent")) unzoom();
98
zoom(target);
99
}
100
else if (e.target.id == "unzoom") unzoom();
101
else if (e.target.id == "search") search_prompt();
102
else if (e.target.id == "ignorecase") toggle_ignorecase();
103
}, false)
104
105
// mouse-over for info
106
// show
107
window.addEventListener("mouseover", function(e) {
108
var target = find_group(e.target);
109
if (target) details.nodeValue = g_to_text(target);
110
}, false)
111
112
// clear
113
window.addEventListener("mouseout", function(e) {
114
var target = find_group(e.target);
115
if (target) details.nodeValue = ' ';
116
}, false)
117
118
// ctrl-F for search
119
window.addEventListener("keydown",function (e) {
120
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
121
e.preventDefault();
122
search_prompt();
123
}
124
}, false)
125
126
// ctrl-I to toggle case-sensitive search
127
window.addEventListener("keydown",function (e) {
128
if (e.ctrlKey && e.keyCode === 73) {
129
e.preventDefault();
130
toggle_ignorecase();
131
}
132
}, false)
133
134
// functions
135
function find_child(node, selector) {
136
var children = node.querySelectorAll(selector);
137
if (children.length) return children[0];
138
return;
139
}
140
function find_group(node) {
141
var parent = node.parentElement;
142
if (!parent) return;
143
if (parent.id == "frames") return node;
144
return find_group(parent);
145
}
146
function orig_save(e, attr, val) {
147
if (e.attributes["_orig_" + attr] != undefined) return;
148
if (e.attributes[attr] == undefined) return;
149
if (val == undefined) val = e.attributes[attr].value;
150
e.setAttribute("_orig_" + attr, val);
151
}
152
function orig_load(e, attr) {
153
if (e.attributes["_orig_"+attr] == undefined) return;
154
e.attributes[attr].value = e.attributes["_orig_" + attr].value;
155
e.removeAttribute("_orig_"+attr);
156
}
157
function g_to_text(e) {
158
var text = find_child(e, "details").firstChild.nodeValue;
159
return (text)
160
}
161
function g_to_func(e) {
162
var child = find_child(e, "rawtext");
163
return child ? child.textContent : null;
164
}
165
function update_text(e) {
166
var r = find_child(e, "rect");
167
var t = find_child(e, "text");
168
var w = parseFloat(r.attributes.width.value) -3;
169
var txt = find_child(e, "rawtext").textContent.replace(/\([^(]*\)$/,"");
170
t.attributes.x.value = parseFloat(r.attributes.x.value) + 3;
171
172
// Smaller than this size won't fit anything
173
if (w < 2 * 12 * 0.59) {
174
t.textContent = "";
175
return;
176
}
177
178
t.textContent = txt;
179
// Fit in full text width
180
if (/^ *$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
181
return;
182
183
for (var x = txt.length - 2; x > 0; x--) {
184
if (t.getSubStringLength(0, x + 2) <= w) {
185
t.textContent = txt.substring(0, x) + "..";
186
return;
187
}
188
}
189
t.textContent = "";
190
}
191
192
// zoom
193
function zoom_reset(e) {
194
if (e.attributes != undefined) {
195
orig_load(e, "x");
196
orig_load(e, "width");
197
}
198
if (e.childNodes == undefined) return;
199
for (var i = 0, c = e.childNodes; i < c.length; i++) {
200
zoom_reset(c[i]);
201
}
202
}
203
function zoom_child(e, x, ratio) {
204
if (e.attributes != undefined) {
205
if (e.attributes.x != undefined) {
206
orig_save(e, "x");
207
e.attributes.x.value = (parseFloat(e.attributes.x.value) - x - 10) * ratio + 10;
208
if (e.tagName == "text")
209
e.attributes.x.value = find_child(e.parentNode, "rect[x]").attributes.x.value + 3;
210
}
211
if (e.attributes.width != undefined) {
212
orig_save(e, "width");
213
e.attributes.width.value = parseFloat(e.attributes.width.value) * ratio;
214
}
215
}
216
217
if (e.childNodes == undefined) return;
218
for (var i = 0, c = e.childNodes; i < c.length; i++) {
219
zoom_child(c[i], x - 10, ratio);
220
}
221
}
222
function zoom_parent(e) {
223
if (e.attributes) {
224
if (e.attributes.x != undefined) {
225
orig_save(e, "x");
226
e.attributes.x.value = 10;
227
}
228
if (e.attributes.width != undefined) {
229
orig_save(e, "width");
230
e.attributes.width.value = parseInt(svg.width.baseVal.value) - (10 * 2);
231
}
232
}
233
if (e.childNodes == undefined) return;
234
for (var i = 0, c = e.childNodes; i < c.length; i++) {
235
zoom_parent(c[i]);
236
}
237
}
238
function zoom(node) {
239
var attr = find_child(node, "rect").attributes;
240
var width = parseFloat(attr.width.value);
241
var xmin = parseFloat(attr.x.value);
242
var xmax = parseFloat(xmin + width);
243
var ymin = parseFloat(attr.y.value);
244
var ratio = (svg.width.baseVal.value - 2 * 10) / width;
245
246
// XXX: Workaround for JavaScript float issues (fix me)
247
var fudge = 0.0001;
248
249
unzoombtn.classList.remove("hide");
250
251
var el = document.getElementById("frames").children;
252
for (var i = 0; i < el.length; i++) {
253
var e = el[i];
254
var a = find_child(e, "rect").attributes;
255
var ex = parseFloat(a.x.value);
256
var ew = parseFloat(a.width.value);
257
var upstack;
258
// Is it an ancestor
259
if ($flip == 1) {
260
upstack = parseFloat(a.y.value) > ymin;
261
} else {
262
upstack = parseFloat(a.y.value) < ymin;
263
}
264
if (upstack) {
265
// Direct ancestor
266
if (ex <= xmin && (ex+ew+fudge) >= xmax) {
267
e.classList.add("parent");
268
zoom_parent(e);
269
update_text(e);
270
}
271
// not in current path
272
else
273
e.classList.add("hide");
274
}
275
// Children maybe
276
else {
277
// no common path
278
if (ex < xmin || ex + fudge >= xmax) {
279
e.classList.add("hide");
280
}
281
else {
282
zoom_child(e, xmin, ratio);
283
update_text(e);
284
}
285
}
286
}
287
search();
288
}
289
function unzoom() {
290
unzoombtn.classList.add("hide");
291
var el = document.getElementById("frames").children;
292
for(var i = 0; i < el.length; i++) {
293
el[i].classList.remove("parent");
294
el[i].classList.remove("hide");
295
zoom_reset(el[i]);
296
update_text(el[i]);
297
}
298
search();
299
}
300
301
// search
302
function toggle_ignorecase() {
303
ignorecase = !ignorecase;
304
if (ignorecase) {
305
ignorecaseBtn.classList.add("show");
306
} else {
307
ignorecaseBtn.classList.remove("show");
308
}
309
reset_search();
310
search();
311
}
312
function reset_search() {
313
var el = document.querySelectorAll("#frames rect");
314
for (var i = 0; i < el.length; i++) {
315
orig_load(el[i], "fill")
316
}
317
}
318
function search_prompt() {
319
if (!searching) {
320
var term = prompt("Enter a search term (regexp " +
321
"allowed, eg: ^ext4_)"
322
+ (ignorecase ? ", ignoring case" : "")
323
+ "\nPress Ctrl-i to toggle case sensitivity", "");
324
if (term != null) {
325
currentSearchTerm = term;
326
search();
327
}
328
} else {
329
reset_search();
330
searching = 0;
331
currentSearchTerm = null;
332
searchbtn.classList.remove("show");
333
searchbtn.firstChild.nodeValue = "Search"
334
matchedtxt.classList.add("hide");
335
matchedtxt.firstChild.nodeValue = ""
336
}
337
}
338
function search(term) {
339
if (currentSearchTerm === null) return;
340
var term = currentSearchTerm;
341
342
var re = new RegExp(term, ignorecase ? 'i' : '');
343
var el = document.getElementById("frames").children;
344
var matches = new Object();
345
var maxwidth = 0;
346
for (var i = 0; i < el.length; i++) {
347
var e = el[i];
348
var func = g_to_func(e);
349
var rect = find_child(e, "rect");
350
if (func == null || rect == null)
351
continue;
352
353
// Save max width. Only works as we have a root frame
354
var w = parseFloat(rect.attributes.width.value);
355
if (w > maxwidth)
356
maxwidth = w;
357
358
if (func.match(re)) {
359
// highlight
360
var x = parseFloat(rect.attributes.x.value);
361
orig_save(rect, "fill");
362
rect.attributes.fill.value = "rgb(230,0,230)";
363
364
// remember matches
365
if (matches[x] == undefined) {
366
matches[x] = w;
367
} else {
368
if (w > matches[x]) {
369
// overwrite with parent
370
matches[x] = w;
371
}
372
}
373
searching = 1;
374
}
375
}
376
if (!searching)
377
return;
378
379
searchbtn.classList.add("show");
380
searchbtn.firstChild.nodeValue = "Reset Search";
381
382
// calculate percent matched, excluding vertical overlap
383
var count = 0;
384
var lastx = -1;
385
var lastw = 0;
386
var keys = Array();
387
for (k in matches) {
388
if (matches.hasOwnProperty(k))
389
keys.push(k);
390
}
391
// sort the matched frames by their x location
392
// ascending, then width descending
393
keys.sort(function(a, b){
394
return a - b;
395
});
396
// Step through frames saving only the biggest bottom-up frames
397
// thanks to the sort order. This relies on the tree property
398
// where children are always smaller than their parents.
399
var fudge = 0.0001; // JavaScript floating point
400
for (var k in keys) {
401
var x = parseFloat(keys[k]);
402
var w = matches[keys[k]];
403
if (x >= lastx + lastw - fudge) {
404
count += w;
405
lastx = x;
406
lastw = w;
407
}
408
}
409
// display matched percent
410
matchedtxt.classList.remove("hide");
411
var pct = 100 * count / maxwidth;
412
if (pct != 100) pct = pct.toFixed(1)
413
matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
414
}
415
]]>
416
</script>
417
<rect x="0.0" y="0" width="1200.0" height="$height.0" fill="url(#background)" />
418
<text id="title" x="600.00" y="24" >$title</text>
419
<text id="unzoom" x="10.00" y="24" class="hide">Reset Zoom</text>
420
<text id="search" x="1090.00" y="24" >Search</text>
421
<text id="ignorecase" x="1174.00" y="24" >ic</text>
422
<text id="matched" x="1090.00" y="$status" > </text>
423
<text id="details" x="10.00" y="$status" > </text>
424
<g id="frames">
425
"""
426
427
def namehash(s):
428
# FNV-1a
429
hval = 0x811c9dc5
430
for ch in s:
431
hval = hval ^ ord(ch)
432
hval = hval * 0x01000193
433
hval = hval % (2 ** 32)
434
return (hval % 31337) / 31337.0
435
436
def display(root, title, colors, flip = False):
437
if colors == "cold":
438
gradient_start = "#eef2ee"
439
gradient_end = "#e0ffe0"
440
else:
441
gradient_start = "#eeeeee"
442
gradient_end = "#eeeeb0"
443
444
maxdepth = 0
445
for n in root.subtree():
446
maxdepth = max(maxdepth, n.depth)
447
448
svgheight = maxdepth * 16 + 3 * 16 + 2 * 16
449
450
print(template
451
.replace("$title", title)
452
.replace("$gradient-start", gradient_start)
453
.replace("$gradient-end", gradient_end)
454
.replace("$height", str(svgheight))
455
.replace("$status", str((svgheight - 16 + 3 if flip else 3 * 16 - 3)))
456
.replace("$flip", str(int(flip)))
457
)
458
459
framewidth = 1200 - 20
460
461
def pixels(x):
462
return float(x) / root.width * framewidth if root.width > 0 else 0
463
464
for n in root.subtree():
465
if pixels(n.width) < 0.1:
466
continue
467
468
x = 10 + pixels(n.offset)
469
y = (maxdepth - 1 - n.depth if flip else n.depth) * 16 + 3 * 16
470
width = pixels(n.width)
471
height = 15
472
473
if colors == "cold":
474
fillr = 0
475
fillg = int(190 + 50 * namehash(n.name))
476
fillb = int(210 * namehash(n.name[::-1]))
477
else:
478
fillr = int(205 + 50 * namehash(n.name))
479
fillg = int(230 * namehash(n.name[::-1]))
480
fillb = int(55 * namehash(n.name[::-2]))
481
482
fill = "rgb({},{},{})".format(fillr, fillg, fillb)
483
chars = width / (12 * 0.59)
484
485
text = n.text()
486
487
if chars >= 3:
488
if chars < len(text):
489
text = text[:int(chars-2)] + ".."
490
else:
491
text = ""
492
493
print("<g>")
494
print("<title>{}</title>".format(escape(n.title())))
495
print("<details>{}</details>".format(escape(n.details(root))))
496
print("<rect x='{}' y='{}' width='{}' height='{}' fill='{}' rx='2' ry='2' />".format(x, y, width, height, fill))
497
print("<text x='{}' y='{}'>{}</text>".format(x + 3, y + 10.5, escape(text)))
498
print("<rawtext>{}</rawtext>".format(escape(n.text())))
499
print("</g>")
500
501
print("</g>\n</svg>\n")
502
503