tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

reftest-analyzer.xhtml (31672B)


      1 <?xml version="1.0" encoding="UTF-8"?>
      2 <!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
      3 <!-- vim: set shiftwidth=2 tabstop=2 autoindent expandtab: -->
      4 <!-- This Source Code Form is subject to the terms of the Mozilla Public
      5   - License, v. 2.0. If a copy of the MPL was not distributed with this
      6   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
      7 <!--
      8 
      9 Features to add:
     10 * make the left and right parts of the viewer independently scrollable
     11 * make the test list filterable
     12 ** default to only showing unexpecteds
     13 * add other ways to highlight differences other than circling?
     14 * add zoom/pan to images
     15 * Add ability to load log via XMLHttpRequest (also triggered via URL param)
     16 * color the test list based on pass/fail and expected/unexpected/random/skip
     17 * ability to load multiple logs ?
     18 ** rename them by clicking on the name and editing
     19 ** turn the test list into a collapsing tree view
     20 ** move log loading into popup from viewer UI
     21 
     22 -->
     23 <!DOCTYPE html>
     24 <html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
     25 <head>
     26  <title>Reftest analyzer</title>
     27  <style type="text/css"><![CDATA[
     28 
     29  html, body { margin: 0; }
     30  html { padding: 0; }
     31  body { padding: 4px; }
     32 
     33  #pixelarea, #itemlist, #images { position: absolute; }
     34  #itemlist, #images { overflow: auto; }
     35  #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
     36  #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
     37  #images { top: 0; bottom: 0; left: 320px; right: 0; }
     38 
     39  #leftpane { width: 320px; }
     40  #images { position: fixed; top: 10px; left: 340px; }
     41 
     42  form#imgcontrols { margin: 0; display: block; }
     43 
     44  #itemlist > table { border-collapse: collapse; }
     45  #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
     46  #itemlist td.activeitem { background-color: yellow; }
     47 
     48  /*
     49  #itemlist > table > tbody > tr.pass > td.url { background: lime; }
     50  #itemlist > table > tbody > tr.fail > td.url { background: red; }
     51  */
     52 
     53  #magnification > svg { display: block; width: 84px; height: 84px; }
     54 
     55  #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
     56  #pixelinfo table { border-collapse: collapse; }
     57  #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
     58  #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
     59 
     60  #pixelhint { display: inline; color: #88f; cursor: help; }
     61  #pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
     62  #pixelhint:hover { color: #000; }
     63  #pixelhint:hover > * { display: block; }
     64  #pixelhint p { margin: 0; }
     65  #pixelhint p + p { margin-top: 1em; }
     66 
     67  ]]></style>
     68  <script type="text/javascript"><![CDATA[
     69 
     70 let heatmapCanvas = null;
     71 let heatmapUMouse;
     72 let gl = null;
     73 
     74 function heatmap_render_setup(canvas) {
     75  gl = canvas.getContext('webgl', {antialias: false, depth: false, preserveDrawingBuffer:false});
     76 
     77  const vertices = [
     78    0, 0,
     79    1, 0,
     80    0, 1,
     81    1, 1,
     82  ];
     83 
     84  const vertexBuffer = gl.createBuffer();
     85  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
     86  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
     87 
     88  const vsCode =
     89    `
     90    attribute vec2 a_vertCoord;
     91    varying   vec2 v_texCoord;
     92    void main(void) {
     93      gl_Position = vec4(2.0 * a_vertCoord - 1.0, 0.0, 1.0);
     94      v_texCoord = a_vertCoord;
     95    }`;
     96 
     97  const VS = gl.createShader(gl.VERTEX_SHADER);
     98  gl.shaderSource(VS, vsCode);
     99  gl.compileShader(VS);
    100 
    101  const psCode =
    102    `
    103    precision mediump float;
    104    uniform vec2 heatmapUMouse;
    105    varying vec2 v_texCoord;
    106    uniform sampler2D u_image1, u_image2;
    107    void main(void) {
    108      vec2 dxy = abs(heatmapUMouse - gl_FragCoord.xy);
    109      if(dxy.x < 1.0 || dxy.y < 1.0) {  // crosshair
    110        gl_FragColor = vec4( 1.0, 1.0, 0.5, 1.0 );
    111        return;
    112      }
    113 
    114      vec3 img1 = texture2D(u_image1, v_texCoord).rgb;
    115      vec3 img2 = texture2D(u_image2, v_texCoord).rgb;
    116 
    117      bool is_top  = gl_FragCoord.y > float(heatmapUMouse.y);
    118      bool is_left = gl_FragCoord.x < float(heatmapUMouse.x);
    119 
    120      vec3 rgb;
    121      if(is_top) {
    122        if(is_left) {
    123          rgb = img1;
    124        } else {
    125          rgb = img2;
    126        }
    127      } else {
    128        vec3 diff = abs(img1 - img2);
    129        if(is_left) {
    130          rgb = diff;
    131        } else {
    132          float max_diff = max(diff.r, max(diff.g, diff.b));
    133          if(max_diff == 0.0) {
    134            rgb = vec3(0.0, 0.0, 0.2);
    135          } else {
    136            // some arbitrary colorization -- transition from green to red
    137            // with some contrast tweaks to make red stand out a bit more
    138            // at about 0.5'ish
    139            rgb = vec3( pow(max_diff, 0.5), pow(1.0 - max_diff, 3.0), 0.0 );
    140          }
    141        }
    142      }
    143 
    144      gl_FragColor = vec4( rgb, 1.0 );
    145    }`;
    146 
    147  const FS = gl.createShader(gl.FRAGMENT_SHADER);
    148  gl.shaderSource(FS, psCode);
    149  gl.compileShader(FS);
    150 
    151  const program = gl.createProgram();
    152  gl.attachShader(program, VS);
    153  gl.attachShader(program, FS);
    154  gl.linkProgram(program);
    155  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    156    console.error('Link failed: ' + gl.getProgramInfoLog(program));
    157    console.error('vs info-log: ' + gl.getShaderInfoLog(VS));
    158    console.error('fs info-log: ' + gl.getShaderInfoLog(FS));
    159    return;  // don't assign heatmapCanvas
    160  }
    161  gl.useProgram(program);
    162 
    163  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    164 
    165  const coord = gl.getAttribLocation(program, "a_vertCoord");
    166  gl.vertexAttribPointer(coord, 2, gl.FLOAT, false, 0, 0);
    167  gl.enableVertexAttribArray(coord);
    168 
    169  heatmapUMouse = gl.getUniformLocation(program, "heatmapUMouse");
    170 
    171  gl.uniform1i(gl.getUniformLocation(program, 'u_image1'), 0);
    172  gl.uniform1i(gl.getUniformLocation(program, 'u_image2'), 1);
    173 
    174  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    175  heatmapCanvas = canvas;
    176 }
    177 
    178 function heatmap_change_image(index, image) {
    179  if (heatmapCanvas === null) {
    180    return;
    181  }
    182  const texture = gl.createTexture();
    183  gl.activeTexture(gl.TEXTURE0 + index);
    184  gl.bindTexture  (gl.TEXTURE_2D, texture);
    185  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    186  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    187  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    188  gl.texImage2D   (gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    189 }
    190 
    191 function heatmap_render(mouse_x, mouse_y) {
    192  if (heatmapCanvas === null) {
    193    return;
    194  }
    195 
    196  gl.uniform2f(heatmapUMouse, mouse_x, mouse_y);
    197 
    198  // the canvas resizes as user selects different reftests
    199  gl.viewport(0, 0, heatmapCanvas.width, heatmapCanvas.height);
    200 
    201  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    202 }
    203 
    204 function heatmap_on_mousemove(mousemove_event) {
    205  if (heatmapCanvas === null) {
    206    return;
    207  }
    208  const rect = heatmapCanvas.getBoundingClientRect();
    209  let x = mousemove_event.clientX - rect.left;
    210  let y = mousemove_event.clientY - rect.top;
    211  x = x * heatmapCanvas.width  / heatmapCanvas.clientWidth;
    212  y = y * heatmapCanvas.height / heatmapCanvas.clientHeight;
    213 
    214  // mouse has Y == 0 at the top, GL has it at the bottom:
    215  const flip_y = heatmapCanvas.height-1 - y;
    216  heatmap_render(x, flip_y);
    217 
    218  return { x:x, y:y };
    219 }
    220 
    221  ]]></script>
    222 
    223  <script type="text/javascript"><![CDATA[
    224 
    225 var XLINK_NS = "http://www.w3.org/1999/xlink";
    226 var SVG_NS = "http://www.w3.org/2000/svg";
    227 var IMAGE_NOT_AVAILABLE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAASCAYAAADczdVTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHy0lEQVRoge2aX2hb5xnGf2dYabROgqQkpMuKnWUJLmxHMFaa/SscteQiF5EvUgqLctEVrDJKK1+MolzkQr4IctgW+SLIheJc1BpFpswJw92FbaZsTCGTL0465AtntUekJdJ8lByVHbnnwLsLKbKdSJbiZBVjeuAYn+/P+z3fc97vfd9zbEVEhB566BK+1m0CPfx/o+eAPXQVbR3QqVapOl8FlR46h0O1Wu02iacCZfsasMKEz8vbx1JYE6fY/dXx6mEbFObPcvDVDBlznpc9G+2r8xNcvLqK2w39r4UI+fs7tFjmytgFFu718865EIebPGincI3zFz7Bcrtx97/GL0P+p+IPbSOgRwXtW3vpewqL/a/g5rgf39hit2m0hGUAHOHrrq3trmef4/lDB7Ay57n01zuPZXPX7jUunv+Yf9ktR7D/0CHca7/n3KXPsHbAuynkCWCZptgiImKLaVqP9NuW1bT9ceybpr3j+WJbYrVa3rbEatGZi2uixvWdrysilmWKae2M+5PqlktoosayLfubcrN10dAk24aynUsIxMVsadwUs+EX7dEyAlaXLqMoCj6fj5HkUqO9MD+Govjx+xXcXi+uoRAhvwuv182Z8Ws4AJUlxoZ8uNxuvF43ii/EtdXNNUuV68lR/IqC4gsxPj7KkE/BF5qmClRXrzFSt+/1ulDOjLNU6eQ4OcyPDqH4hhg5O4LicuN2K4xcvk6jjHUKJM8O1fvcKMoZkouFOq1VPp1OcuXGAvrvfsv0lWmSySTzN0sdH+jyYhK/ouB2e/G6XfjPJikBVG8SUhT8fl99nwVGfQp+vx+f4iO5VO1AtwJjfgXF58M/kqSVJP9ef0xuAI6NlwWmL41xxqeg+PyMXr72yBqW3cI4JaZHh1DcXrxeLy5liORiB7q1PiZFyeV0mQqz9TRZeUmFVUGLSjqdkgCIFp2RTCosEJOiiIihSyKWkDl9WYrFnCQCCNF0w0QmHhBQJTEzJ+nZSQmAoEYks2KIGBkJgASiM5I3LbGMnCSCCEQl38GJMvMZiag1e+nlFcmmIgKaZEwREaPGhWGZ1VfEMFZkNj4sgCSyhoihSzwSlqCGoAUlEo1IJByW+Oxyh+dZJJ+eklhiRnIrRcnrM6KCxLOmiNiipyICSGR2pTY2O1m7T2XEsNrrJmJLfjkn6amwoMbFaMEhG28eAVtzExErW3sOBCWVzkpmNiEqCOEZ2RyLTT3eJAKaMhVEUMOSXjHEtg3JTIUFkNTK9rGwbQrWm2xGb6QoWxIqEtdtEWO28aDtoi6JSFCAjUtL1AUzJA4SSW/IZ2VjjU0V0zEBJBiJSzwWk1g8IZEAAmrdidrBkoSKxB4IW08tGVNEzIxoIJM5a8v4SQ1RY5lGSy6x8xScz6QkHFBre1Zre49nH+y1KDEQLV7TcyU1LBCtHVppp9smxk2dYAMtHXA7blZWNJDZ4sZ4MxPbdHjrbc3WNuvOq4YlkYhLLBaXeKx2sLcrBUS2ScFtUbUBh3WgajvgOYgGuKjw4Rsqb1uvkssbWLbJXFQFqL/I9IEKa2WzYcqy16E2BNteB1R+cuwoRwcHGRx4nlfenWMuPclRDx3goSraqd+7Gj/Y5d76SrXLu3VKLYW1rMZbo/QpB4+9zt6fT1I0Law/LRMBaLzC7ePNuSgL7/2GpcotLr7+AZG5t9gH0Fa3zuFq1tiWG4DKs5tebV1NDDW1XYd26iWO9A8wODjAUfUN5ubm+Ch4ZFuuLRzQoVwqUCqXyN9fg3tFSuUShVIZhyr5O2vo94o42DwD/PP23fq8Bf5urLO+BoHBwxzc20c++wcmz+lAkWLFATwcf3+YDwIDhMYmuDw+wt5j5+C5ZwDYP/gSoLP6xX5+fOIkJ47/lIP8g49/Nc3tDj59OZUiRR3uFYsAVO/eZoE1yvkyeA6gAaff+zU3SxUcp8LilQucnoFTP3hhix19/garlQqFW9eZOBti9Mqt9mubXwBw+NALeDC4cfVDzgP3i3keUN/nf4uo+hEver/DRaK84/9mY/72uoFTKVMolVn5/HPgPvlSmVKhRL2bSrlEqVyidH8N/d7t2u/lakfcKneLgM4rvxhncbXA6tI8kTffB+0NjnrAqZYplcrk83ceXdtzgB+psHD7S/pfPs7JkydQB1x8dnWS2SVje9GaxkVLl+DmNNC4NJn/S6JxH5nJyNRwrW7Qi7oMgxBMyd9molvmRKO1cExgshG6l9NTEhkOynAkLlOJoKBuhPV8ZlK0h9aNTqVbv3ltEK/VIiAQEN0yZVLbuM+aImLoEgts3VdsJrfFil1M1/ZSv9RAROaWO8n/hkyF1Q3bgeFGygvPrDRG5Wcf1IJbq9rlNrrNbra96aqlUVMSWrNnNiw5uw23T/4o4Xq7FtA29h2My3K9WtETgRZr13UxdIk+pGswkpCcsX0N2OZD9BOgWqFsgWePp20KWb0ywkDgEIa8y55Gq0O5XKHP7cGz++l/haxWylgOuD17aG7eoVpxwL27RX8b27jZ42n1qdahXKrg2bfnUW0eQ7edoD232l+/LPp2pHvNfh8eT2f8/3sO2AZLyRAvns6gqToLOgxP6Uz87HvdoNJDF9E1B6ysLrLw5yW+3PUNvv3dH/L9wX3doNFDl9E1B+yhB+j9O1YPXcZ/AAl9BWJNvZE7AAAAAElFTkSuQmCC";
    228 
    229 var gPhases = null;
    230 
    231 var gIDCache = {};
    232 
    233 var gMagPixPaths = [];     // 2D array of array-of-two <path> objects used in the pixel magnifier
    234 var gMagWidth = 5;         // number of zoomed in pixels to show horizontally
    235 var gMagHeight = 5;        // number of zoomed in pixels to show vertically
    236 var gMagZoom = 16;         // size of the zoomed in pixels
    237 var gImage1Data;           // ImageData object for the reference image
    238 var gImage2Data;           // ImageData object for the test output image
    239 var gFlashingPixels = [];  // array of <path> objects that should be flashed due to pixel color mismatch
    240 var gParams;
    241 
    242 function ID(id) {
    243  if (!(id in gIDCache))
    244    gIDCache[id] = document.getElementById(id);
    245  return gIDCache[id];
    246 }
    247 
    248 function hash_parameters() {
    249  var result = { };
    250  var params = window.location.hash.substr(1).split(/[&;]/);
    251  for (var i = 0; i < params.length; i++) {
    252    var parts = params[i].split("=");
    253    result[parts[0]] = unescape(unescape(parts[1]));
    254  }
    255  return result;
    256 }
    257 
    258 function load() {
    259  gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
    260  build_mag();
    261  gParams = hash_parameters();
    262  if (gParams.log) {
    263    show_phase("loading");
    264    process_log(gParams.log);
    265  } else if (gParams.logurl) {
    266    show_phase("loading");
    267    var req = new XMLHttpRequest();
    268    req.onreadystatechange = function() {
    269      if (req.readyState === 4) {
    270        process_log(req.responseText);
    271      }
    272    };
    273    req.open('GET', gParams.logurl, true);
    274    req.send();
    275  }
    276  window.addEventListener('keypress', handle_keyboard_shortcut);
    277  ID("image1").addEventListener('error', image_load_error);
    278  ID("image2").addEventListener('error', image_load_error);
    279 }
    280 
    281 function image_load_error(e) {
    282  e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
    283 }
    284 
    285 function build_mag() {
    286  var mag = ID("mag");
    287 
    288  var r = document.createElementNS(SVG_NS, "rect");
    289  r.setAttribute("x", gMagZoom * -gMagWidth / 2);
    290  r.setAttribute("y", gMagZoom * -gMagHeight / 2);
    291  r.setAttribute("width", gMagZoom * gMagWidth);
    292  r.setAttribute("height", gMagZoom * gMagHeight);
    293  mag.appendChild(r);
    294 
    295  mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
    296 
    297  for (var x = 0; x < gMagWidth; x++) {
    298    gMagPixPaths[x] = [];
    299    for (var y = 0; y < gMagHeight; y++) {
    300      var p1 = document.createElementNS(SVG_NS, "path");
    301      p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
    302      p1.setAttribute("stroke", "black");
    303      p1.setAttribute("stroke-width", "1px");
    304      p1.setAttribute("fill", "#aaa");
    305 
    306      var p2 = document.createElementNS(SVG_NS, "path");
    307      p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
    308      p2.setAttribute("stroke", "black");
    309      p2.setAttribute("stroke-width", "1px");
    310      p2.setAttribute("fill", "#888");
    311 
    312      mag.appendChild(p1);
    313      mag.appendChild(p2);
    314      gMagPixPaths[x][y] = [p1, p2];
    315    }
    316  }
    317 
    318  var flashedOn = false;
    319  setInterval(function() {
    320    flashedOn = !flashedOn;
    321    flash_pixels(flashedOn);
    322  }, 500);
    323 }
    324 
    325 function show_phase(phaseid) {
    326  for (var i in gPhases) {
    327    var phase = gPhases[i];
    328    phase.style.display = (phase.id == phaseid) ? "" : "none";
    329  }
    330 
    331  if (phase == "viewer")
    332    ID("images").style.display = "none";
    333 }
    334 
    335 function fileentry_changed() {
    336  show_phase("loading");
    337  var input = ID("fileentry");
    338  var files = input.files;
    339  if (files.length > 0) {
    340    // Only handle the first file; don't handle multiple selection.
    341    // The parts of the log we care about are ASCII-only.  Since we
    342    // can ignore lines we don't care about, best to read in as
    343    // iso-8859-1, which guarantees we don't get decoding errors.
    344    var fileReader = new FileReader();
    345    fileReader.onload = function(e) {
    346      var log = null;
    347 
    348      log = e.target.result;
    349 
    350      if (log)
    351        process_log(log);
    352      else
    353        show_phase("entry");
    354    }
    355    fileReader.readAsText(files[0], "iso-8859-1");
    356  }
    357  // So the user can process the same filename again (after
    358  // overwriting the log), clear the value on the form input so we
    359  // will always get an onchange event.
    360  input.value = "";
    361 }
    362 
    363 function log_pasted() {
    364  show_phase("loading");
    365  var entry = ID("logentry");
    366  var log = entry.value;
    367  entry.value = "";
    368  process_log(log);
    369 }
    370 
    371 var gTestItems;
    372 
    373 function process_log(contents) {
    374  var lines = contents.split(/[\r\n]+/);
    375  gTestItems = [];
    376  for (var j in lines) {
    377    var line = lines[j];
    378    // Ignore duplicated output in logcat.
    379    if (line.match(/I\/Gecko.*?REFTEST/))
    380      continue;
    381    var match = line.match(/^(?:.*? (?:INFO|ERROR) -\s+)?(?:REFTEST\s+)?(.*)$/);
    382    if (!match)
    383      continue;
    384    line = match[1];
    385    match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/);
    386    if (match) {
    387      var state = match[1];
    388      var random = match[2];
    389      var url = match[3];
    390      var extra = match[4];
    391      gTestItems.push(
    392        {
    393          pass: !state.match(/DEBUG-INFO$|FAIL$/),
    394          // only one of the following three should ever be true
    395          unexpected: !!state.match(/^TEST-UNEXPECTED/),
    396          random: (random == "(EXPECTED RANDOM)"),
    397          skip: (extra == " (SKIP)"),
    398          url: url,
    399          images: [],
    400          imageLabels: []
    401        });
    402      continue;
    403    }
    404    match = line.match(/IMAGE([^:]*): (data:.*)$/);
    405    if (match) {
    406      var item = gTestItems[gTestItems.length - 1];
    407      item.images.push(match[2]);
    408      item.imageLabels.push(match[1]);
    409    }
    410  }
    411 
    412  build_viewer();
    413 }
    414 
    415 function build_viewer() {
    416  if (gTestItems.length == 0) {
    417    show_phase("entry");
    418    return;
    419  }
    420 
    421  var cell = ID("itemlist");
    422  while (cell.childNodes.length > 0)
    423    cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
    424 
    425  var table = document.createElement("table");
    426  var tbody = document.createElement("tbody");
    427  table.appendChild(tbody);
    428 
    429  for (var i in gTestItems) {
    430    var item = gTestItems[i];
    431 
    432    // optional url filter for only showing unexpected results
    433    if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
    434      continue;
    435 
    436    // XXX regardless skip expected pass items until we have filtering UI
    437    if (item.pass && !item.unexpected)
    438      continue;
    439 
    440    var tr = document.createElement("tr");
    441    var rowclass = item.pass ? "pass" : "fail";
    442    var td;
    443    var text;
    444 
    445    td = document.createElement("td");
    446    text = "";
    447    if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
    448    if (item.random) { text += "R"; rowclass += " random"; }
    449    if (item.skip) { text += "S"; rowclass += " skip"; }
    450    td.appendChild(document.createTextNode(text));
    451    tr.appendChild(td);
    452 
    453    td = document.createElement("td");
    454    td.id = "item" + i;
    455    td.className = "url";
    456    // Only display part of URL after "/mozilla/".
    457    var match = item.url.match(/\/mozilla\/(.*)/);
    458    text = document.createTextNode(match ? match[1] : item.url);
    459    if (item.images.length > 0) {
    460      var a = document.createElement("a");
    461      a.href = "javascript:show_images(" + i + ")";
    462      a.appendChild(text);
    463      td.appendChild(a);
    464    } else {
    465      td.appendChild(text);
    466    }
    467    tr.appendChild(td);
    468 
    469    tbody.appendChild(tr);
    470  }
    471 
    472  cell.appendChild(table);
    473 
    474  show_phase("viewer");
    475 }
    476 
    477 function get_image_data(src, whenReady) {
    478  var img = new Image();
    479  img.onload = function() {
    480    var canvas = document.createElement("canvas");
    481    canvas.width = img.naturalWidth;
    482    canvas.height = img.naturalHeight;
    483 
    484    var ctx = canvas.getContext("2d");
    485    ctx.drawImage(img, 0, 0);
    486 
    487    whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
    488  };
    489  img.src = src;
    490 }
    491 
    492 function sync_svg_size(imageData) {
    493  // We need the size of the 'svg' and its 'image' elements to match the size
    494  // of the ImageData objects that we're going to read pixels from or else our
    495  // magnify() function will be very broken.
    496  ID("svg").setAttribute("width", imageData.width);
    497  ID("svg").setAttribute("height", imageData.height);
    498 }
    499 
    500 function sync_heatmap_size(imageData) {
    501  ID("heat_canvas").setAttribute("width" , imageData.width);
    502  ID("heat_canvas").setAttribute("height", imageData.height);
    503 }
    504 
    505 function show_images(i) {
    506  var item = gTestItems[i];
    507  var cell = ID("images");
    508 
    509  // Remove activeitem class from any existing elements
    510  var activeItems = document.querySelectorAll(".activeitem");
    511  for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
    512    activeItems[activeItemIdx].classList.remove("activeitem");
    513  }
    514 
    515  ID("item" + i).classList.add("activeitem");
    516  ID("image1").style.display = "";
    517  ID("image2").style.display = "none";
    518  show_diff_none();
    519  ID("imgcontrols").reset();
    520  ID("diffcontrols").reset();
    521 
    522  ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
    523  // Making the href be #image1 doesn't seem to work
    524  ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
    525  if (item.images.length == 1) {
    526    ID("imgcontrols").style.display = "none";
    527  } else {
    528    ID("imgcontrols").style.display = "";
    529 
    530    ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
    531    // Making the href be #image2 doesn't seem to work
    532    ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
    533 
    534    ID("label1").textContent = 'Image ' + item.imageLabels[0];
    535    ID("label2").textContent = 'Image ' + item.imageLabels[1];
    536  }
    537 
    538  cell.style.display = "";
    539 
    540  get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); sync_heatmap_size(gImage1Data); heatmap_change_image(0, gImage1Data); });
    541  get_image_data(item.images[1], function(data) { gImage2Data = data; heatmap_change_image(1, gImage2Data); });
    542 }
    543 
    544 function show_image(i) {
    545  if (i == 1) {
    546    ID("image1").style.display = "";
    547    ID("image2").style.display = "none";
    548  } else {
    549    ID("image1").style.display = "none";
    550    ID("image2").style.display = "";
    551  }
    552 }
    553 
    554 function handle_keyboard_shortcut(event) {
    555  switch (event.charCode) {
    556  case 49: // "1" key
    557    document.getElementById("radio1").checked = true;
    558    show_image(1);
    559    break;
    560  case 50: // "2" key
    561    document.getElementById("radio2").checked = true;
    562    show_image(2);
    563    break;
    564  case 100: // "d" key
    565    document.getElementById("radio_diff_circle").click();
    566    break;
    567  case 104: // "h" key
    568    document.getElementById("radio_diff_heatmap").click();
    569    break;
    570  case 112: // "p" key
    571    shift_images(-1);
    572    break;
    573  case 110: // "n" key
    574    shift_images(1);
    575    break;
    576  }
    577 }
    578 
    579 function shift_images(dir) {
    580  var activeItem = document.querySelector(".activeitem");
    581  if (!activeItem) {
    582    return;
    583  }
    584  for (var elm = activeItem; elm; elm = elm.parentElement) {
    585    if (elm.tagName != "tr") {
    586      continue;
    587    }
    588    elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
    589    if (elm) {
    590      elm.getElementsByTagName("a")[0].click();
    591    }
    592    return;
    593  }
    594 }
    595 
    596 function show_diff_none() {
    597  ID("svg")        .style.display = "";
    598  ID("diffrect")   .style.display = "none";
    599  ID("heat_canvas").style.display = "none";
    600 }
    601 
    602 function show_diff_circle() {
    603  ID("svg")        .style.display = "";
    604  ID("diffrect")   .style.display = "";
    605  ID("heat_canvas").style.display = "none";
    606 }
    607 
    608 function show_diff_heatmap() {
    609  ID("svg")        .style.display = "none";
    610  ID("diffrect")   .style.display = "none";
    611  ID("heat_canvas").style.display = "";
    612 
    613  if (heatmapCanvas === null) {
    614    canvas = document.getElementById('heat_canvas');
    615    heatmap_render_setup(canvas);
    616    heatmap_change_image(0, gImage1Data);
    617    heatmap_change_image(1, gImage2Data);
    618    heatmap_render(0, 0);
    619 
    620    window.addEventListener('mousemove', e => {
    621      var { x: x, y: y } = heatmap_on_mousemove(e);
    622      magnify_around(Math.floor(x), Math.floor(y));
    623    });
    624  }
    625 }
    626 
    627 function flash_pixels(on) {
    628  var stroke = on ? "red" : "black";
    629  var strokeWidth = on ? "2px" : "1px";
    630  for (var i = 0; i < gFlashingPixels.length; i++) {
    631    gFlashingPixels[i].setAttribute("stroke", stroke);
    632    gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
    633  }
    634 }
    635 
    636 function cursor_point(evt) {
    637  var m = evt.target.getScreenCTM().inverse();
    638  var p = ID("svg").createSVGPoint();
    639  p.x = evt.clientX;
    640  p.y = evt.clientY;
    641  p = p.matrixTransform(m);
    642  return { x: Math.floor(p.x), y: Math.floor(p.y) };
    643 }
    644 
    645 function hex2(i) {
    646  return (i < 16 ? "0" : "") + i.toString(16);
    647 }
    648 
    649 function canvas_pixel_as_hex(data, x, y) {
    650  var offset = (y * data.width + x) * 4;
    651  var r = data.data[offset];
    652  var g = data.data[offset + 1];
    653  var b = data.data[offset + 2];
    654  return "#" + hex2(r) + hex2(g) + hex2(b);
    655 }
    656 
    657 function hex_as_rgb(hex) {
    658  return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
    659 }
    660 
    661 function magnify(evt) {
    662  var { x: x, y: y } = cursor_point(evt);
    663  magnify_around(x, y);
    664 }
    665 
    666 function magnify_around(x, y) {
    667  if (x < 0 || y < 0 || x >= gImage1Data.width || y >= gImage1Data.height) {
    668    return;
    669  }
    670  var centerPixelColor1, centerPixelColor2;
    671 
    672  var dx_lo = -Math.floor(gMagWidth / 2);
    673  var dx_hi = Math.floor(gMagWidth / 2);
    674  var dy_lo = -Math.floor(gMagHeight / 2);
    675  var dy_hi = Math.floor(gMagHeight / 2);
    676 
    677  flash_pixels(false);
    678  gFlashingPixels = [];
    679  for (var j = dy_lo; j <= dy_hi; j++) {
    680    for (var i = dx_lo; i <= dx_hi; i++) {
    681      var px = x + i;
    682      var py = y + j;
    683      var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
    684      var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
    685      // Here we just use the dimensions of gImage1Data since we expect test
    686      // and reference to have the same dimensions.
    687      if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
    688        p1.setAttribute("fill", "#aaa");
    689        p2.setAttribute("fill", "#888");
    690      } else {
    691        var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
    692        var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
    693        p1.setAttribute("fill", color1);
    694        p2.setAttribute("fill", color2);
    695        if (color1 != color2) {
    696          gFlashingPixels.push(p1, p2);
    697          p1.parentNode.appendChild(p1);
    698          p2.parentNode.appendChild(p2);
    699        }
    700        if (i == 0 && j == 0) {
    701          centerPixelColor1 = color1;
    702          centerPixelColor2 = color2;
    703        }
    704      }
    705    }
    706  }
    707  flash_pixels(true);
    708  show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
    709 }
    710 
    711 function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
    712  var pixelinfo = ID("pixelinfo");
    713  ID("coords").textContent = [x, y];
    714  ID("pix1hex").textContent = pix1hex;
    715  ID("pix1rgb").textContent = pix1rgb;
    716  ID("pix2hex").textContent = pix2hex;
    717  ID("pix2rgb").textContent = pix2rgb;
    718 }
    719 
    720  ]]></script>
    721 
    722 </head>
    723 <body onload="load()">
    724 
    725 <div id="entry">
    726 
    727 <h1>Reftest analyzer: load reftest log</h1>
    728 
    729 <p>Either paste your log into this textarea:<br />
    730 <textarea cols="80" rows="10" id="logentry"/><br/>
    731 <input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
    732 
    733 <p>... or load it from a file:<br/>
    734 <input type="file" id="fileentry" onchange="fileentry_changed()" />
    735 </p>
    736 </div>
    737 
    738 <div id="loading" style="display:none">Loading log...</div>
    739 
    740 <div id="viewer" style="display:none">
    741  <div id="pixelarea">
    742    <div id="pixelinfo">
    743      <table>
    744        <tbody>
    745          <tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
    746          <tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
    747          <tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
    748        </tbody>
    749      </table>
    750      <div>
    751        <div id="pixelhint">    752          <div>
    753            <p>Move the mouse over the reftest image on the right to show
    754            magnified pixels on the left.  The color information above is for
    755            the pixel centered in the magnified view.</p>
    756            <p>Image 1 is shown in the upper triangle of each pixel and Image 2
    757            is shown in the lower triangle.</p>
    758          </div>
    759        </div>
    760      </div>
    761    </div>
    762    <div id="magnification">
    763      <svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
    764        <g id="mag"/>
    765      </svg>
    766    </div>
    767  </div>
    768  <div id="itemlist"></div>
    769  <div id="images" style="display:none">
    770    <form id="imgcontrols">
    771    <input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label>
    772    <input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)"                   /><label id="label2" title="2" for="radio2">Image 2</label>
    773    </form>
    774 
    775    <form id="diffcontrols">
    776      Differences:
    777      <input id="radio_diff_none"    name="diff" value="0" type="radio" onchange="show_diff_none()" checked="checked"/>
    778        <label for="radio_diff_none">None</label>
    779      <input id="radio_diff_circle"  name="diff" value="1" type="radio" onchange="show_diff_circle()" />
    780        <label for="radio_diff_circle">Circle</label>
    781      <input id="radio_diff_heatmap" name="diff" value="2" type="radio" onchange="show_diff_heatmap()" />
    782        <label for="radio_diff_heatmap">Heatmap</label>
    783    </form>
    784 
    785    <canvas width="800" height="1000" id="heat_canvas" style="display:none;"></canvas>
    786 
    787    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="1000" id="svg">
    788      <defs>
    789        <!-- use sRGB to avoid loss of data -->
    790        <filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
    791                style="color-interpolation-filters: sRGB">
    792          <feImage id="feimage1" result="img1" xlink:href="#image1" />
    793          <feImage id="feimage2" result="img2" xlink:href="#image2" />
    794          <!-- inv1 and inv2 are the images with RGB inverted -->
    795          <feComponentTransfer result="inv1" in="img1">
    796            <feFuncR type="linear" slope="-1" intercept="1" />
    797            <feFuncG type="linear" slope="-1" intercept="1" />
    798            <feFuncB type="linear" slope="-1" intercept="1" />
    799          </feComponentTransfer>
    800          <feComponentTransfer result="inv2" in="img2">
    801            <feFuncR type="linear" slope="-1" intercept="1" />
    802            <feFuncG type="linear" slope="-1" intercept="1" />
    803            <feFuncB type="linear" slope="-1" intercept="1" />
    804          </feComponentTransfer>
    805          <!-- w1 will have non-white pixels anywhere that img2
    806               is brighter than img1, and w2 for the reverse.
    807               It would be nice not to have to go through these
    808               intermediate states, but feComposite
    809               type="arithmetic" can't transform the RGB channels
    810               and leave the alpha channel untouched. -->
    811          <feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
    812          <feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
    813          <!-- c1 will have non-black pixels anywhere that img2
    814               is brighter than img1, and c2 for the reverse -->
    815          <feComponentTransfer result="c1" in="w1">
    816            <feFuncR type="linear" slope="-1" intercept="1" />
    817            <feFuncG type="linear" slope="-1" intercept="1" />
    818            <feFuncB type="linear" slope="-1" intercept="1" />
    819          </feComponentTransfer>
    820          <feComponentTransfer result="c2" in="w2">
    821            <feFuncR type="linear" slope="-1" intercept="1" />
    822            <feFuncG type="linear" slope="-1" intercept="1" />
    823            <feFuncB type="linear" slope="-1" intercept="1" />
    824          </feComponentTransfer>
    825          <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
    826          <feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
    827          <!-- a will be opaque for every pixel with differences and transparent for all others -->
    828          <feColorMatrix result="a" type="matrix" values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1 1 1 0 0" />
    829 
    830          <!-- a, dilated by 1 pixel -->
    831          <feMorphology result="dila1" in="a" operator="dilate" radius="1" />
    832          <!-- a, dilated by 2 pixels -->
    833          <feMorphology result="dila2" in="dila1" operator="dilate" radius="1" />
    834 
    835          <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
    836          <feComposite result="highlight" in="dila2" in2="dila1" operator="out" />
    837 
    838          <feFlood result="red" flood-color="red" />
    839          <feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
    840          <feFlood result="black" flood-color="black" flood-opacity="0.5" />
    841          <feMerge>
    842            <feMergeNode in="black" />
    843            <feMergeNode in="redhighlight" />
    844          </feMerge>
    845        </filter>
    846      </defs>
    847      <g onmousemove="magnify(evt)">
    848        <image x="0" y="0" width="100%" height="100%" id="image1" />
    849        <image x="0" y="0" width="100%" height="100%" id="image2" />
    850      </g>
    851      <rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
    852    </svg>
    853  </div>
    854 </div>
    855 
    856 </body>
    857 </html>