diff --git a/default.css b/default.css index 062217e..b9937ac 100644 --- a/default.css +++ b/default.css @@ -9,3 +9,8 @@ html, body { body .main { height: 100%; } + +.main #c { + width: 100%; + height: 100%; +} diff --git a/geometry.js b/geometry.js new file mode 100644 index 0000000..51cc80c --- /dev/null +++ b/geometry.js @@ -0,0 +1,65 @@ +let colors = {}; + +function get_color(stage_name) { + if (stage_name in colors) { + return colors[stage_name]; + } + + const r = Math.floor(Math.random() * 155); + const g = Math.floor(Math.random() * 155); + const b = Math.floor(Math.random() * 155); + + colors[stage_name] = { 'r': 100 + r, 'g': 100 + g, 'b': 100 + b }; + + return colors[stage_name]; +} + +function generate(trace_id) { + const result = { + 'count': 0, + }; + + const positions = []; + const sizes = []; + const colors = []; + + let instructions = []; + + if (trace_id in traces) { + instructions = traces[trace_id]; + } + + console.log(instructions); + + let y = 0; + + for (const instruction of instructions) { + for (let i = 0; i < instruction.lanes['0'].length; ++i) { + const stage = instruction.lanes['0'][i]; + let stage_cycles; + + if (i < instruction.lanes['0'].length - 1) { + const next_stage = instruction.lanes['0'][i + 1]; + stage_cycles = next_stage.c - stage.c; + } else { + stage_cycles = instruction.retcyc - stage.c; + } + + const color = get_color(stage.name); + + sizes.push(stage_cycles * config.w, 1 * config.h); + positions.push(config.w * stage.c, config.h * y); + colors.push(color.r, color.g, color.b, 255); + + result.count++; + } + + ++y; + } + + result.pos = new Float32Array(positions); + result.size = new Float32Array(sizes); + result.color = new Uint8Array(colors); + + return result; +} diff --git a/index.html b/index.html index 4b71c67..a828e3f 100644 --- a/index.html +++ b/index.html @@ -15,9 +15,11 @@ +
+
diff --git a/index.js b/index.js index 5d31e11..ccea8ae 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,8 @@ document.addEventListener('DOMContentLoaded', main); +let traces = {}; + function main() { + init_webgl(); init_listeners(); } diff --git a/input.js b/input.js index defadf1..fd4c47c 100644 --- a/input.js +++ b/input.js @@ -1,6 +1,14 @@ function init_listeners() { document.querySelector('.main').addEventListener('dragover', cancel); document.querySelector('.main').addEventListener('drop', drop); + document.querySelector('.main').addEventListener('click', debug); + + window.addEventListener('keydown', keydown); + window.addEventListener('keyup', keyup); +} + +function debug() { + schedule_draw(); } function cancel(e) { @@ -8,6 +16,27 @@ function cancel(e) { e.stopPropagation(); } +function keydown(e) { + if (e.code === 'ArrowLeft') { + offset.x += 10 / scale; + } else if (e.code === 'ArrowRight') { + offset.x -= 10 / scale; + } else if (e.code === 'ArrowDown') { + offset.y -= 10 / scale; + } else if (e.code === 'ArrowUp') { + offset.y += 10 / scale; + } else if (e.code === 'PageUp') { + scale *= 0.9; + } else if (e.code === 'PageDown') { + scale *= 1.1; + } + + schedule_draw(); +} + +function keyup(e) { +} + function drop(e) { e.preventDefault(); diff --git a/parse.js b/parse.js index cfe283f..17e3289 100644 --- a/parse.js +++ b/parse.js @@ -5,16 +5,23 @@ function parse(text) { let line_start = 0; let line_index = 0; + let c = -1; + + const instructions = []; //console.log(text); for (let i = 0; i < text.length; ++i) { - const c = text[i]; - - if (c === '\n') { + if (text[i] === '\n') { + if (line_index === 0) { + line_start = i + 1; + line_index += 1; + continue; + } + // TODO: speed const line_copy = text.substring(line_start, i); - const line_parts = line_copy.split(/\s+/); + const line_parts = line_copy.split(/\t/); if (line_parts.length === 0) { console.error('Parser error: empty line'); @@ -23,30 +30,22 @@ function parse(text) { const command = line_parts[0]; switch (command) { - case 'Kanata': { - if (!assert_arglen(line_parts, 1, 'Kanata')) return false; - - const version = Number(line_parts[1]); - if (version !== 4) { - console.error('Parser error: only Kanata traces version 4 are supported'); - return false; - } - - break; - } - case 'C=': { + // Specify the number of cycles since the start of simulation. if (!assert_arglen(line_parts, 1, 'C=')) return false; const cycles = Number(line_parts[1]); + c = cycles; break; } case 'C': { + // Specifies the number of elapsed cycles since the last output of any commands. if (!assert_arglen(line_parts, 1, 'C')) return false; const cycles = Number(line_parts[1]); + c += cycles; break; } @@ -58,6 +57,16 @@ function parse(text) { const insn_id_in_sim = Number(line_parts[2]); const thread_id = Number(line_parts[3]); + instructions[insn_id_in_file] = { + 'cycle': c, + 'file_id': insn_id_in_file, + 'sim_id': insn_id_in_sim, + 'thread_id': thread_id, + 'text': '', + 'popover_text': '', + 'lanes': {}, + }; + break; } @@ -66,7 +75,18 @@ function parse(text) { const id = Number(line_parts[1]); const type = Number(line_parts[2]); - const text = line_parts.slice(3).join(' '); // TODO: preserve spacing, newlines + const text = line_parts[3].replaceAll('\\n', '\n'); + + if (id in instructions) { + if (type === 0) { + instructions[id].text += text; + } else { + instructions[id].popover_text += text; + } + } else { + console.error('Parser error: label for an instruction that does not exist'); + return false; + } break; } @@ -78,6 +98,22 @@ function parse(text) { const lane = Number(line_parts[2]); const stage = line_parts[3]; + + if (id in instructions) { + if (!(lane in instructions[id].lanes)) { + instructions[id].lanes[lane] = []; + } + + instructions[id].lanes[lane].push({ + 'name': stage, + 'c': c, + }); + } else { + console.error('Parser error: pipeline start for an instruction that does not exist'); + return false; + } + + break; } @@ -88,6 +124,8 @@ function parse(text) { const lane = Number(line_parts[2]); const stage = line_parts[3]; + // This command can be ommited + break; } @@ -98,6 +136,18 @@ function parse(text) { const retire_id = Number(line_parts[2]); const type = Number(line_parts[3]); + if (id in instructions) { + if (type === 0) { + instructions[id].retired = true; + } else { + instructions[id].retired = false; + } + instructions[id].retcyc = c; + } else { + console.error('Parser error: retire for an instruction that does not exist'); + return false; + } + break; } @@ -107,6 +157,8 @@ function parse(text) { const consumer_id = Number(line_parts[1]); const producer_id = Number(line_parts[2]); const type = Number(line_parts[3]); + + // TODO break; } @@ -116,7 +168,6 @@ function parse(text) { return false; } } - //console.log(command); line_start = i + 1; line_index += 1; @@ -126,6 +177,10 @@ function parse(text) { const after = performance.now(); console.log(`Parsed in ${Math.round(after - before)}ms`); + + traces['0'] = instructions; + + return true; } function assert_arglen(args, arglen, command) { diff --git a/render.js b/render.js index e69de29..6427b58 100644 --- a/render.js +++ b/render.js @@ -0,0 +1,250 @@ +let programs = {}; +let buffers = {}; +let timers = {}; +let config = { + bytes_per_quad: 20, + w: 24, + h: 24, +}; + +let canvas = null; +let gl = null; +let gpu_timer_ext = null; +let offset = { x: 0, y: 0 }; +let scale = 1; + +const tquad_vs_src = `#version 300 es + in vec2 a_pos; + in vec2 a_size; + in vec4 a_color; + + uniform vec2 u_res; + uniform vec2 u_translation; + uniform float u_scale; + + out vec4 v_color; + + void main() { + int vertex_index = gl_VertexID % 6; + vec2 corner; + + if (vertex_index == 0) { + // "top left" aka "p1" + corner = a_pos + vec2(1.0); + } else if (vertex_index == 1 || vertex_index == 5) { + // "top right" aka "p2" + corner = a_pos + vec2(a_size.x - 1.0, 1.0); + } else if (vertex_index == 2 || vertex_index == 4) { + // "bottom left" aka "p3" + corner = a_pos + vec2(1.0, a_size.y - 1.0); + } else { + // "bottom right" aka "p4" + corner = a_pos + a_size - vec2(1.0); + } + + vec2 screen02 = (corner.xy * vec2(u_scale) + u_translation) / u_res * 2.0; + screen02.y = 2.0 - screen02.y; + v_color = a_color; + + gl_Position = vec4(screen02 - 1.0, 1.0, 1.0); + } +`; + +const tquad_fs_src = `#version 300 es + precision highp float; + + in vec4 v_color; + + layout(location = 0) out vec4 FragColor; + + void main() { + FragColor = v_color; + } +`; + +function schedule_draw() { + if (!timers.raf) { + window.requestAnimationFrame(draw); + timers.raf = true; + } +} + +function draw() { + const cpu_before = performance.now(); + const width = window.innerWidth; + const height = window.innerHeight; + + let query = null; + + if (gpu_timer_ext !== null) { + query = gl.createQuery(); + gl.beginQuery(gpu_timer_ext.TIME_ELAPSED_EXT, query); + } + + gl.viewport(0, 0, canvas.width, canvas.height); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + const quads = generate('0'); + + if (quads.count > 0) { + const program = programs['quad']; + + gl.useProgram(program.program); + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); + gl.bufferData(gl.ARRAY_BUFFER, quads.count * config.bytes_per_quad, gl.STATIC_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, quads.pos); + gl.bufferSubData(gl.ARRAY_BUFFER, quads.pos.byteLength, quads.size); + gl.bufferSubData(gl.ARRAY_BUFFER, quads.pos.byteLength + quads.size.byteLength, quads.color); + + gl.uniform2f(program.locations['u_res'], canvas.width, canvas.height); + gl.uniform2f(program.locations['u_translation'], offset.x, offset.y); + gl.uniform1f(program.locations['u_scale'], scale); + + gl.enableVertexAttribArray(program.locations['a_pos']); + gl.enableVertexAttribArray(program.locations['a_size']); + gl.enableVertexAttribArray(program.locations['a_color']); + + gl.vertexAttribPointer(program.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, 0); + gl.vertexAttribPointer(program.locations['a_size'], 2, gl.FLOAT, false, 2 * 4, quads.pos.byteLength); + gl.vertexAttribPointer(program.locations['a_color'], 4, gl.UNSIGNED_BYTE, true, 4 * 1, quads.pos.byteLength + quads.size.byteLength); + + gl.vertexAttribDivisor(program.locations['a_pos'], 1); + gl.vertexAttribDivisor(program.locations['a_size'], 1); + gl.vertexAttribDivisor(program.locations['a_color'], 1); + + gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quads.count); + + gl.vertexAttribDivisor(program.locations['a_pos'], 0); + gl.vertexAttribDivisor(program.locations['a_size'], 0); + gl.vertexAttribDivisor(program.locations['a_color'], 0); + } + + if (gpu_timer_ext) { + gl.endQuery(gpu_timer_ext.TIME_ELAPSED_EXT); + + const next_tick = () => { + if (query) { + const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); + const disjoint = gl.getParameter(gpu_timer_ext.GPU_DISJOINT_EXT); + + if (available && !disjoint) { + const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT); + console.log('Last GPU Frametime: ' + Math.round(timeElapsed / 10000) / 100 + 'ms'); + } + + if (available || disjoint) { + gl.deleteQuery(query); + query = null; + } else if (!available) { + setTimeout(next_tick, 0); + } + } + } + + setTimeout(next_tick, 0); + } + + const cpu_after = performance.now(); + + timers.raf = false; + + console.log('Last CPU Frametime: ' + Math.round((cpu_after - cpu_before) * 100) / 100 + 'ms'); +} + + +function init_webgl() { + canvas = document.querySelector('#c'); + gl = canvas.getContext('webgl2'); + + gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); + if (gpu_timer_ext === null) { + gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query'); + } + + const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); + const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); + + programs = { + 'quad': create_program(gl, quad_vs, quad_fs), + }; + + buffers = { + 'b_packed': gl.createBuffer(), + }; + + const resize_canvas = (entries) => { + // https://www.khronos.org/webgl/wiki/HandlingHighDPI + const entry = entries[0]; + + let width; + let height; + + if (entry.devicePixelContentBoxSize) { + width = entry.devicePixelContentBoxSize[0].inlineSize; + height = entry.devicePixelContentBoxSize[0].blockSize; + } else if (entry.contentBoxSize) { + // fallback for Safari that will not always be correct + width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio); + height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio); + } + + canvas.width = width; + canvas.height = height; + + schedule_draw(); + } + + const resize_observer = new ResizeObserver(resize_canvas); + resize_observer.observe(canvas); +} + +function create_shader(gl, type, source) { + const shader = gl.createShader(type); + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + return shader; + } + + console.error(type, ':', gl.getShaderInfoLog(shader)); + + gl.deleteShader(shader); +} + +function create_program(gl, vs, fs) { + const program = gl.createProgram(); + + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.linkProgram(program); + + if (gl.getProgramParameter(program, gl.LINK_STATUS)) { + // src: tiny-sdf + // https://github.com/mapbox/tiny-sdf + + const wrapper = {program}; + const num_attrs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); + const num_uniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); + + wrapper.locations = {}; + + for (let i = 0; i < num_attrs; i++) { + const attribute = gl.getActiveAttrib(program, i); + wrapper.locations[attribute.name] = gl.getAttribLocation(program, attribute.name); + } + + for (let i = 0; i < num_uniforms; i++) { + const uniform = gl.getActiveUniform(program, i); + wrapper.locations[uniform.name] = gl.getUniformLocation(program, uniform.name); + } + + return wrapper; + } + + console.error('link:', gl.getProgramInfoLog(program)); + + gl.deleteProgram(program); +}