Update rive-cpp to 2.0 version
[platform/core/uifw/rive-tizen.git] / submodule / skia / src / utils / SkVMVisualizer.cpp
1 /*
2  * Copyright 2021 Google LLC
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 #include "src/utils/SkVMVisualizer.h"
8
9 #include "include/core/SkStream.h"
10 #include "include/private/SkOpts_spi.h"
11 #include "src/core/SkStreamPriv.h"
12 #include "src/sksl/tracing/SkVMDebugTrace.h"
13
14 #include <algorithm>
15 #include <sstream>
16 #include <stdarg.h>
17 #include <string>
18 #include <utility>
19
20 namespace {
21
22 size_t get_addr(const char* str) {
23     size_t addr;
24     std::istringstream ss(str);
25     ss >> std::hex >> addr;
26     SkASSERT(!ss.fail());
27     return addr;
28 }
29
30 }
31
32 namespace skvm::viz {
33
34 bool Instruction::operator == (const Instruction& o) const {
35     return this->kind == o.kind &&
36            this->startCode == o.startCode &&
37            this->endCode == o.endCode &&
38            this->instructionIndex == o.instructionIndex &&
39            this->instruction == o.instruction &&
40            this->duplicates == o.duplicates;
41 }
42
43 SkString Instruction::classes() const {
44     SkString result((kind & InstructionFlags::kDead) ? "dead" : "normal");
45     if (duplicates > 0) result += " origin";
46     if (duplicates < 0) result += " deduped";
47     return result;
48 }
49
50 uint32_t InstructionHash::operator()(const Instruction& i) const {
51     uint32_t hash = 0;
52     hash = SkOpts::hash_fn(&i.kind, sizeof(i.kind), hash);
53     hash = SkOpts::hash_fn(&i.instructionIndex, sizeof(i.instructionIndex), hash);
54     hash = SkOpts::hash_fn(&i.instruction, sizeof(i.instruction), hash);
55     return hash;
56 }
57
58 void Visualizer::parseDisassembler(SkWStream* output, const char* code) {
59     if (code == nullptr) {
60         fAsmLine = 0;
61         return;
62     }
63     // Read the disassembled code from <_skvm_jit> until
64     // the last command that is attached to the byte code
65     // We skip all the prelude (main loop organizing and such)
66     // generate the main loop running on vector values (keeping hoisted commands in place)
67     // and skip the tail loop (which is the same as the main, only on scalar values)
68     // We stop after the last byte code.
69     SkTArray<SkString> commands;
70     SkStrSplit(code, "\n", kStrict_SkStrSplitMode, &commands);
71     for (const SkString& line : commands) {
72         ++fAsmLine;
73         if (line.find("<_skvm_jit>") >= 0) {
74             break;
75         }
76     }
77
78     if (fAsmLine < commands.size()) {
79         const SkString& line = commands[fAsmLine];
80         SkTArray<SkString> tokens;
81         SkStrSplit(line.c_str(), "\t", kStrict_SkStrSplitMode, &tokens);
82         if (tokens.size() >= 2 && tokens[0].size() > 1) {
83             fAsmStart = get_addr(tokens[0].c_str());
84         }
85     }
86
87     fAsmEnd += fAsmStart;
88     for (size_t i = fAsmLine; i < commands.size(); ++i) {
89         const SkString& line = commands[i];
90         SkTArray<SkString> tokens;
91         SkStrSplit(line.c_str(), "\t", kStrict_SkStrSplitMode, &tokens);
92         size_t addr = 0;
93         if (tokens.size() >= 2 && tokens[0].size() > 1) {
94             addr = get_addr(tokens[0].c_str());
95         }
96         if (addr > fAsmEnd) {
97             break;
98         }
99         addr -= fAsmStart;
100         if (!fAsm.empty()) {
101             MachineCommand& prev = fAsm.back();
102             if (prev.command.isEmpty()) {
103                 int len = addr - prev.address;
104                 prev.command.printf("{ align %d bytes }", len);
105             }
106         }
107         SkString command;
108         for (size_t t = 2; t < tokens.size(); ++t) {
109             command += tokens[t];
110         }
111         fAsm.push_back({addr, tokens[0], command, tokens[1]});
112     }
113     if (!fAsm.empty()) {
114         MachineCommand& prev = fAsm.back();
115         if (prev.command.isEmpty()) {
116             int len = fInstructions.back().endCode - prev.address;
117             prev.command.printf("{ align %d bytes }", len);
118         }
119     }
120     fAsmLine = 0;
121 }
122
123 void Visualizer::dump(SkWStream* output, const char* code) {
124     SkDebugfStream stream;
125     fOutput = output ? output : &stream;
126     this->parseDisassembler(output, code);
127     this->dumpHead();
128     for (size_t id = 0ul; id < fInstructions.size(); ++id) {
129         this->dumpInstruction(id);
130     }
131     this->dumpTail();
132 }
133
134 void Visualizer::markAsDeadCode(std::vector<bool>& live, const std::vector<int>& newIds) {
135     for (size_t id = 0ul; id < fInstructions.size(); ++id) {
136         Instruction& instruction = fInstructions[id];
137         if (instruction.instructionIndex < 0) {
138             // We skip commands that are duplicates of some other commands
139             // They either will be dead or alive together with the origin
140             continue;
141         }
142         SkASSERT(instruction.instructionIndex < (int)live.size());
143         if (live[instruction.instructionIndex]) {
144             instruction.instructionIndex = newIds[instruction.instructionIndex];
145             fToDisassembler[instruction.instructionIndex] = id;
146         } else {
147             instruction.kind
148                     = static_cast<InstructionFlags>(instruction.kind | InstructionFlags::kDead);
149             fToDisassembler[instruction.instructionIndex] = -1;
150             // Anything negative meaning the command is duplicate/dead
151             instruction.instructionIndex = -2;
152         }
153     }
154 }
155
156 void Visualizer::addInstructions(std::vector<skvm::Instruction>& program) {
157     for (Val id = 0; id < (Val)program.size(); id++) {
158         skvm::Instruction& instr = program[id];
159         auto isDuplicate = instr.op == Op::duplicate;
160         if (isDuplicate) {
161             this->markAsDuplicate(instr.immA, id);
162             instr = program[instr.immA];
163         }
164         this->addInstruction({
165             viz::InstructionFlags::kNormal,
166             /*startCode=*/0, /*endCode=0*/0,
167             id,
168             isDuplicate ? -1 : 0,
169             instr
170         });
171     }
172 }
173
174 void Visualizer::addInstruction(Instruction skvm) {
175     if (!touches_varying_memory(skvm.instruction.op)) {
176         if (auto found = fIndex.find(skvm)) {
177             auto& instruction = fInstructions[*found];
178             ++(instruction.duplicates);
179             return;
180         }
181     }
182     fIndex.set(skvm, fInstructions.size());
183     fToDisassembler.set(skvm.instructionIndex, fInstructions.size());
184     fInstructions.emplace_back(std::move(skvm));
185 }
186
187 void Visualizer::finalize(const std::vector<skvm::Instruction>& all,
188                               const std::vector<skvm::OptimizedInstruction>& optimized) {
189     for (Val id = 0; id < (Val)all.size(); id++) {
190         if (optimized[id].can_hoist) {
191             size_t found = fToDisassembler[id];
192             Instruction& instruction = fInstructions[found];
193             instruction.kind =
194                     static_cast<InstructionFlags>(instruction.kind | InstructionFlags::kHoisted);
195         }
196     }
197 }
198
199 void Visualizer::addMachineCommands(int id, size_t start, size_t end) {
200     size_t found = fToDisassembler[id];
201     Instruction& instruction = fInstructions[found];
202     instruction.startCode = start;
203     instruction.endCode = end;
204     fAsmEnd = std::max(fAsmEnd, end);
205 }
206
207 SkString Visualizer::V(int reg) const {
208     if (reg == -1) {
209         return SkString("{optimized}");
210     } else if (reg == -2) {
211         return SkString("{dead code}");
212     } else {
213         return SkStringPrintf("v%d", reg);
214     }
215 }
216
217 void Visualizer::formatVV(const char* op, int v1, int v2) const {
218     this->writeText("%s %s, %s", op, V(v1).c_str(), V(v2).c_str());
219 }
220 void Visualizer::formatPV(const char* op, int imm, int v1) const {
221     this->writeText("%s Ptr%d, %s", op, imm, V(v1).c_str());
222 }
223 void Visualizer::formatPVV(const char* op, int imm, int v1, int v2) const {
224     this->writeText("%s Ptr%d, %s, %s", op, imm, V(v1).c_str(), V(v2).c_str());
225 }
226 void Visualizer::formatPVVVV(const char* op, int imm, int v1, int v2, int v3, int v4) const {
227     this->writeText("%s Ptr%d, %s, %s, %s, %s",
228               op, imm, V(v1).c_str(), V(v2).c_str(), V(v3).c_str(), V(v4).c_str());
229 }
230 void Visualizer::formatA_(int id, const char* op) const {
231     writeText("%s = %s", V(id).c_str(), op);
232 }
233 void Visualizer::formatA_P(int id, const char* op, int imm) const {
234     this->writeText("%s = %s Ptr%d", V(id).c_str(), op, imm);
235 }
236 void Visualizer::formatA_PH(int id, const char* op, int immA, int immB) const {
237     this->writeText("%s = %s Ptr%d, %x", V(id).c_str(), op, immA, immB);
238 }
239 void Visualizer::formatA_PHH(int id, const char* op, int immA, int immB, int immC) const {
240     this->writeText("%s = %s Ptr%d, %x, %x", V(id).c_str(), op, immA, immB, immC);
241 }
242 void Visualizer::formatA_PHV(int id, const char* op, int immA, int immB, int v) const {
243     this->writeText("%s = %s Ptr%d, %x, %s", V(id).c_str(), op, immA, immB, V(v).c_str());
244 }
245 void Visualizer::formatA_S(int id, const char* op, int imm) const {
246     float f;
247     memcpy(&f, &imm, 4);
248     char buffer[kSkStrAppendScalar_MaxSize];
249     char* stop = SkStrAppendScalar(buffer, f);
250     this->writeText("%s = %s %x (", V(id).c_str(), op, imm);
251     fOutput->write(buffer, stop - buffer);
252     this->writeText(")");
253 }
254 void Visualizer::formatA_V(int id, const char* op, int v) const {
255     this->writeText("%s = %s %s", V(id).c_str(), op, V(v).c_str());
256 }
257 void Visualizer::formatA_VV(int id, const char* op, int v1, int v2) const {
258     this->writeText("%s = %s %s, %s", V(id).c_str(), op, V(v1).c_str(), V(v2).c_str());
259 }
260 void Visualizer::formatA_VVV(int id, const char* op,  int v1, int v2, int v3) const {
261     this->writeText(
262             "%s = %s %s, %s, %s", V(id).c_str(), op, V(v1).c_str(), V(v2).c_str(), V(v3).c_str());
263 }
264 void Visualizer::formatA_VC(int id, const char* op,  int v, int imm) const {
265     this->writeText("%s = %s %s, %d", V(id).c_str(), op, V(v).c_str(), imm);
266 }
267
268 void Visualizer::writeText(const char* format, ...) const {
269     SkString message;
270     va_list argp;
271     va_start(argp, format);
272     message.appendVAList(format, argp);
273     va_end(argp);
274     fOutput->writeText(message.c_str());
275 }
276
277 void Visualizer::dumpInstruction(int id0) const {
278     const Instruction& instruction = fInstructions[id0];
279     const int id = instruction.instructionIndex;
280     const int x = instruction.instruction.x,
281               y = instruction.instruction.y,
282               z = instruction.instruction.z,
283               w = instruction.instruction.w;
284     const int immA = instruction.instruction.immA,
285               immB = instruction.instruction.immB,
286               immC = instruction.instruction.immC;
287     if (instruction.instruction.op == skvm::Op::trace_line) {
288         SkASSERT(fDebugInfo != nullptr);
289         SkASSERT(immA >= 0 && immB <= (int)fDebugInfo->fSource.size());
290         this->writeText("<tr class='source'><td class='mask'></td><td colspan=2>// %s</td></tr>\n",
291                         fDebugInfo->fSource[immB].c_str());
292         return;
293     } else if (instruction.instruction.op == skvm::Op::trace_var ||
294                instruction.instruction.op == skvm::Op::trace_scope) {
295         // TODO: We can add some visualization here
296         return;
297     } else if (instruction.instruction.op == skvm::Op::trace_enter) {
298         SkASSERT(fDebugInfo != nullptr);
299         SkASSERT(immA >= 0 && immA <= (int)fDebugInfo->fFuncInfo.size());
300         std::string& func = fDebugInfo->fFuncInfo[immA].name;
301         SkString mask;
302         mask.printf(immC == 1 ? "%s(-1)" : "%s", V(x).c_str());
303         this->writeText(
304                 "<tr class='source'><td class='mask'>&#8618;%s</td><td colspan=2>%s</td></tr>\n",
305                 mask.c_str(),
306                 func.c_str());
307         return;
308     } else if (instruction.instruction.op == skvm::Op::trace_exit) {
309         SkASSERT(fDebugInfo != nullptr);
310         SkASSERT(immA >= 0 && immA <= (int)fDebugInfo->fFuncInfo.size());
311         std::string& func = fDebugInfo->fFuncInfo[immA].name;
312         SkString mask;
313         mask.printf(immC == 1 ? "%s(-1)" : "%s", V(x).c_str());
314         this->writeText(
315                 "<tr class='source'><td class='mask'>&#8617;%s</td><td colspan=2>%s</td></tr>\n",
316                 mask.c_str(),
317                 func.c_str());
318         return;
319     }
320     // No label, to the operation
321     SkString label;
322     if ((instruction.kind & InstructionFlags::kHoisted) != 0) {
323         label.set("&#8593;&#8593;&#8593; ");
324     }
325     if (instruction.duplicates > 0) {
326         label.appendf("*%d", instruction.duplicates + 1);
327     }
328     SkString classes = instruction.classes();
329     this->writeText("<tr class='%s'><td>%s</td><td>", classes.c_str(), label.c_str());
330     // Operation
331     switch (instruction.instruction.op) {
332         case skvm::Op::assert_true:   formatVV("assert_true", x, y);                  break;
333         case skvm::Op::store8:        formatPV("store8", immA, x);                    break;
334         case skvm::Op::store16:       formatPV("store16", immA, x);                   break;
335         case skvm::Op::store32:       formatPV("store32", immA, x);                   break;
336         case skvm::Op::store64:       formatPVV("store64", immA, x, y);               break;
337         case skvm::Op::store128:      formatPVVVV("store128", immA, x, y, z, w);      break;
338         case skvm::Op::index:         formatA_(id, "index");                          break;
339         case skvm::Op::load8:         formatA_P(id, "load8", immA);                   break;
340         case skvm::Op::load16:        formatA_P(id, "load16", immA);                  break;
341         case skvm::Op::load32:        formatA_P(id, "load32", immA);                  break;
342         case skvm::Op::load64:        formatA_PH(id, "load64", immA, immB);           break;
343         case skvm::Op::load128:       formatA_PH(id, "load128", immA, immB);          break;
344         case skvm::Op::gather8:       formatA_PHV(id, "gather8", immA, immB, x);      break;
345         case skvm::Op::gather16:      formatA_PHV(id, "gather16", immA, immB, x);     break;
346         case skvm::Op::gather32:      formatA_PHV(id, "gather32", immA, immB, x);     break;
347         case skvm::Op::uniform32:     formatA_PH(id, "uniform32", immA, immB);        break;
348         case skvm::Op::array32:       formatA_PHH(id, "array32", immA, immB, immC);   break;
349         case skvm::Op::splat:         formatA_S(id, "splat", immA);                   break;
350         case skvm::Op:: add_f32:      formatA_VV(id, "add_f32", x, y);                break;
351         case skvm::Op:: sub_f32:      formatA_VV(id, "sub_f32", x, y);                break;
352         case skvm::Op:: mul_f32:      formatA_VV(id, "mul_f32", x, y);                break;
353         case skvm::Op:: div_f32:      formatA_VV(id, "div_f32", x, y);                break;
354         case skvm::Op:: min_f32:      formatA_VV(id, "min_f32", x, y);                break;
355         case skvm::Op:: max_f32:      formatA_VV(id, "max_f32", x, y);                break;
356         case skvm::Op:: fma_f32:      formatA_VVV(id, "fma_f32", x, y, z);            break;
357         case skvm::Op:: fms_f32:      formatA_VVV(id, "fms_f32", x, y, z);            break;
358         case skvm::Op::fnma_f32:      formatA_VVV(id, "fnma_f32", x, y, z);           break;
359         case skvm::Op::sqrt_f32:      formatA_V(id, "sqrt_f32", x);                   break;
360         case skvm::Op:: eq_f32:       formatA_VV(id, "eq_f32", x, y);                 break;
361         case skvm::Op::neq_f32:       formatA_VV(id, "neq_f32", x, y);                break;
362         case skvm::Op:: gt_f32:       formatA_VV(id, "gt_f32", x, y);                 break;
363         case skvm::Op::gte_f32:       formatA_VV(id, "gte_f32", x, y);                break;
364         case skvm::Op::add_i32:       formatA_VV(id, "add_i32", x, y);                break;
365         case skvm::Op::sub_i32:       formatA_VV(id, "sub_i32", x, y);                break;
366         case skvm::Op::mul_i32:       formatA_VV(id, "mul_i32", x, y);                break;
367         case skvm::Op::shl_i32:       formatA_VC(id, "shl_i32", x, immA);             break;
368         case skvm::Op::shr_i32:       formatA_VC(id, "shr_i32", x, immA);             break;
369         case skvm::Op::sra_i32:       formatA_VC(id, "sra_i32", x, immA);             break;
370         case skvm::Op::eq_i32:        formatA_VV(id, "eq_i32", x, y);                 break;
371         case skvm::Op::gt_i32:        formatA_VV(id, "gt_i32", x, y);                 break;
372         case skvm::Op::bit_and:       formatA_VV(id, "bit_and", x, y);                break;
373         case skvm::Op::bit_or:        formatA_VV(id, "bit_or", x, y);                 break;
374         case skvm::Op::bit_xor:       formatA_VV(id, "bit_xor", x, y);                break;
375         case skvm::Op::bit_clear:     formatA_VV(id, "bit_clear", x, y);              break;
376         case skvm::Op::select:        formatA_VVV(id, "select", x, y, z);             break;
377         case skvm::Op::ceil:          formatA_V(id, "ceil", x);                       break;
378         case skvm::Op::floor:         formatA_V(id, "floor", x);                      break;
379         case skvm::Op::to_f32:        formatA_V(id, "to_f32", x);                     break;
380         case skvm::Op::to_fp16:       formatA_V(id, "to_fp16", x);                    break;
381         case skvm::Op::from_fp16:     formatA_V(id, "from_fp16", x);                  break;
382         case skvm::Op::trunc:         formatA_V(id, "trunc", x);                      break;
383         case skvm::Op::round:         formatA_V(id, "round", x);                      break;
384         default: SkASSERT(false);
385     }
386     // Generation
387     if ((instruction.kind & InstructionFlags::kDead) == 0) {
388         struct Compare
389         {
390             bool operator() (const MachineCommand& c, std::pair<size_t, size_t> p) const
391                             { return c.address < p.first; }
392             bool operator() (std::pair<size_t, size_t> p, const MachineCommand& c) const
393                             { return p.second <= c.address; }
394         };
395
396         std::pair<size_t, size_t> range(instruction.startCode, instruction.endCode);
397         auto commands = std::equal_range(fAsm.begin(), fAsm.end(), range, Compare{ });
398         for (const MachineCommand* line = commands.first; line != commands.second; ++line) {
399             this->writeText("</td></tr>\n<tr class='machine'><td>%s</td><td colspan='2'>%s",
400                             line->label.c_str(),
401                             line->command.c_str());
402         }
403         fAsmLine = commands.second - fAsm.begin();
404     }
405     this->writeText("</td></tr>\n");
406 }
407
408 void Visualizer::dumpHead() const {
409     this->writeText("%s",
410     "<html>\n"
411     "<head>\n"
412     "   <title>SkVM Disassembler Output</title>\n"
413     "   <style>\n"
414     "   button { border-style: none; font-size: 10px; background-color: lightpink; }\n"
415     "   table { text-align: left; }\n"
416     "   table th { background-color: lightgray; }\n"
417     "   .dead, .dead1 { color: lightgray; text-decoration: line-through; }\n"
418     "   .normal, .normal1 { }\n"
419     "   .origin, .origin1 { font-weight: bold; }\n"
420     "   .source, .source1 { color: darkblue; }\n"
421     "   .mask, .mask1 { color: green; }\n"
422     "   .comments, .comments1 { }\n"
423     "   .machine, .machine1 { color: lightblue; }\n"
424     "   </style>\n"
425     "    <script>\n"
426     "    function initializeButton(className) {\n"
427     "      var btn = document.getElementById(className);\n"
428     "      var elems = document.getElementsByClassName(className);\n"
429     "      if (elems == undefined || elems.length == 0) {\n"
430     "        btn.disabled = true;\n"
431     "        btn.innerText = \"None\";\n"
432     "        btn.style.background = \"lightgray\";\n"
433     "        return;\n"
434     "      }\n"
435     "    };\n"
436     "    function initialize() {\n"
437     "      initializeButton('normal');\n"
438     "      initializeButton('source');\n"
439     "      initializeButton('dead');\n"
440     "      initializeButton('machine');\n"
441     "    };\n"
442     "  </script>\n"
443     "</head>\n"
444     "<body onload='initialize();'>\n"
445     "    <script>\n"
446     "    function toggle(btn, className) {\n"
447     "      var elems = document.getElementsByClassName(className);\n"
448     "      for (var i = 0; i < elems.length; i++) {\n"
449     "        var elem = elems.item(i);\n"
450     "        if (elem.style.display === \"\") {\n"
451     "            elem.style.display = \"none\";\n"
452     "            btn.innerText = \"Show\";\n"
453     "            btn.style.background = \"lightcyan\";\n"
454     "        } else {\n"
455     "            elem.style.display = \"\";\n"
456     "            btn.innerText = \"Hide\";\n"
457     "            btn.style.background = \"lightpink\";\n"
458     "        }\n"
459     "      }\n"
460     "    };\n"
461     "    </script>"
462     "    <table border=\"0\" style='font-family:\"monospace\"; font-size: 10px;'>\n"
463     "     <caption style='font-family:Roboto; font-size:15px; text-align:left;'>Legend</caption>\n"
464     "     <tr>\n"
465     "        <th style=\"min-width:100px;\"><u>Kind</u></th>\n"
466     "        <th style=\"width:35%;\"><u>Example</u></th>\n"
467     "        <th style=\"width: 5%; min-width:50px;\"><u></u></th>\n"
468     "        <th style=\"width:60%;\"><u>Description</u></th>\n"
469     "     </tr>\n"
470     "      <tr class='normal1'>"
471             "<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>"
472             "<td>v1 = load32 Ptr1</td>"
473             "<td><button id='normal' onclick=\"toggle(this, 'normal')\">Hide</button></td>"
474             "<td>A regular SkVM command</td></tr>\n"
475     "      <tr class='normal1 origin1'><td>*{N}</td>"
476             "<td>v9 = gt_f32 v0, v1</td>"
477             "<td><button id='dead' onclick=\"toggle(this, 'deduped')\">Hide</button></td>"
478             "<td>A {N} times deduped SkVM command</td></tr>\n"
479     "      <tr class='normal1'><td>&#8593;&#8593;&#8593; &nbsp;&nbsp;&nbsp;</td>"
480             "<td>v22 = splat 3f800000 (1)</td><td></td>"
481             "<td>A hoisted SkVM command</td></tr>\n"
482     "      <tr class='source1'><td class='mask'>mask&#8618;v{N}(-1)</td>"
483             "<td>// C++ source line</td><td></td>"
484             "<td>Enter into the procedure with mask v{N} (which has a constant value -1)"
485             "</td></tr>\n"
486     "      <tr class='source1'><td class='mask'>mask&#8617;v{N}</td>"
487             "<td>// C++ source line</td><td>"
488             "</td><td>Exit the procedure with mask v{N}</td></tr>\n"
489     "      <tr class='source1'><td class='mask'></td><td>// C++ source line</td>"
490             "<td><button id='source' onclick=\"toggle(this, 'source')\">Hide</button></td>"
491             "<td>Line trace back to C++ code</td></tr>\n"
492     "      <tr class='dead1'><td></td><td>{dead code} = mul_f32 v1, v18</td>"
493             "<td><button id='dead' onclick=\"toggle(this, 'dead')\">Hide</button></td>"
494             "<td>An eliminated \"dead code\" SkVM command</td></tr>\n"
495     "      <tr class='machine1'><td>{address}</td><td>vmovups (%rsi),%ymm0</td>"
496             "<td><button id='machine' onclick=\"toggle(this, 'machine')\">Hide</button></td>"
497             "<td>A disassembled machine command generated by SkVM command</td></tr>\n"
498     "    </table>\n"
499     "    <table border = \"0\"style='font-family:\"monospace\"; font-size: 10px;'>\n"
500     "     <caption style='font-family:Roboto;font-size:15px;text-align:left;'>SkVM Code</caption>\n"
501     "     <tr>\n"
502     "        <th style=\"min-width:100px;\"><u>Kind</u></th>\n"
503     "        <th style=\"width:40%;min-width:100px;\"><u>Command</u></th>\n"
504     "        <th style=\"width:60%;\"><u>Comments</u></th>\n"
505     "     </tr>");
506 }
507 void Visualizer::dumpTail() const {
508     this->writeText(
509     "      </table>\n"
510             "</body>\n"
511             "</html>"
512     );
513 }
514 } // namespace skvm::viz