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