chromium/third_party/blink/manual_tests/dom/dom-parts-api-4.html

<!DOCTYPE html>

<p>This is the <b>"Minimal"</b> version of this benchmark.</p>
<fieldset style="display:flex; border:0" id="controls">
  <div>
    <button id="run_baseline"></button>
    <pre id=baseline_log></pre>
    <input type=number hidden id=last_baseline_total value=0 autocomplete=off>
  </div>
  <div>
    <button id="run_parts"></button>
    <pre id=parts_log></pre>
    <input type=number hidden id=last_parts_total value=0 autocomplete=off>
  </div>
</fieldset>
<pre id=overall_log></pre>

<table style="display:none">
  <tbody>
  </tbody>
</table>

<script type="text/javascript">
const nRows = 1000;
const nRuns = 500;
document.querySelector(
  "button#run_baseline"
).textContent = `BASELINE: Create ${nRows} rows, ${nRuns} runs`;
document.querySelector(
  "button#run_parts"
).textContent = `PARTS: Create ${nRows} rows, ${nRuns} runs`;

// Feature detection.
if (typeof document.getPartRoot !== "function") {
  document.write('<h3>Error: The DOM Parts API is not supported in this browser. Please enable Experimental Web Platform Features.</h3>');
}
if (!self.gc) {
  document.write('<h3>Error: This benchmark requires <pre style="display:inline">--js-flags="--expose-gc"</pre> on the command line.</h3>');
}

function template(tplStr) {
  const tplEl = document.createElement("template");
  tplEl.parseparts = true;
  tplEl.innerHTML = tplStr;
  return tplEl;
}
const tbodyEl = document.querySelector("tbody");
const rowTplBaseline = template(
  `<tr><td class="td-num-1"><div class=decoration><span>content</span></div></td><td class="td-num2"><div class=flex><span>Some text</span><a href="#">link text</a></div></td><td class="td-num-3"><span></span><span><a href="#"></span><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td><td class="td-num-4"><div><span><div>content</div></span></div></td></tr>`
);
// The space at the front of this template works around crbug.com/1490375, and can be removed once that's fixed:
const rowTplParts = template(
  ` <tr {{}}><td class="td-num-1"><div class=decoration><span {{}}>content</span></div></td><td class="td-num-2"><div class=flex><span>Some text</span><a {{}} href="#">link text</a></div></td><td class="td-num-3"><span></span><span><a {{}} href="#"></span><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td><td class="td-num-4"><div><span><div {{}}>content</div></span></div></td></tr>`
);



const partRoot = rowTplParts.content.getPartRoot();

let tbodyPart;
function resetTable() {
  const c1 = document.createComment("");
  const c2 = document.createComment("");
  tbodyEl.replaceChildren(c1,c2);
  document.getPartRoot().getParts().forEach(part => part.disconnect());
  tbodyPart = new ChildNodePart(document.getPartRoot(),c1,c2);
  self.gc && self.gc();
}

let selected = 5;

function createRowBaseline(i) {
  const perfs = [];
  let now = performance.now();
  let next = now;
  // this code is run in the CREATE mode

  // step (1): clone template
  const tplClone = rowTplBaseline.content.cloneNode(true);
  next = performance.now();
  perfs.push(next - now); // cloning
  now = next;
  // step (2): find references to the "interesting" elements
  perfs.push(0); // getNodePartNodes
  const trEl = tplClone.firstChild;
  const labelTxtNode = trEl.firstChild.firstChild.firstChild.firstChild;
  // I don't actually know if frameworks do this level of optimization:
  const secondTd = trEl.firstChild.nextSibling;
  const aEl1 = secondTd.firstChild.firstChild.nextSibling;
  const thirdTd = secondTd.nextSibling;
  const aEl2 = thirdTd.firstChild.nextSibling.firstChild;
  const fourthTd = thirdTd.nextSibling;
  const divEl = fourthTd.firstChild.firstChild.firstChild;
  next = performance.now();
  perfs.push(next - now); // accessNodes
  now = next;

  // step (3): add event listeners
  aEl1.addEventListener("click", () => {
    console.log("click first");
  });
  aEl2.addEventListener("click", () => {
    console.log("click second");
  });

  // this code is run in the UPDATE mode
  if (i === selected) {
    trEl.classList.add("table-danger");
  }
  labelTxtNode.nodeValue = "foo " + i;
  divEl.textContent = 'replacement';
  next = performance.now();
  perfs.push(next - now); // operations
  now = next;
  return [tplClone, perfs];
}

function createRowParts(i) {
  const perfs = [];
  let now = performance.now();
  let next = now;
  // this code is run in the CREATE mode

  // step (1): clone template
  const tplCloneRoot = rowTplParts.content.getPartRoot().clone();
  const tplClone = tplCloneRoot.rootContainer
  next = performance.now();
  perfs.push(next - now); // cloning
  now = next;

  // step (2): get parts
  const partNodes = tplCloneRoot.getNodePartNodes();
  next = performance.now();
  perfs.push(next - now); // getNodePartNodes
  now = next;

  const trEl = partNodes[0];
  const labelTxtNode = partNodes[1].firstChild;
  const aEl1 = partNodes[2];
  const aEl2 = partNodes[3];
  const divEl = partNodes[4];
  next = performance.now();
  perfs.push(next - now); // accessNodes
  now = next;

  // step (3): add event listeners
  aEl1.addEventListener("click", () => {
    console.log("click first");
  });
  aEl2.addEventListener("click", () => {
    console.log("click second");
  });
  // this code is run in the UPDATE mode
  if (i === selected) {
    trEl.classList.add('table-danger');
  }
  labelTxtNode.nodeValue = "foo " + i;
  divEl.textContent = 'replacement';
  next = performance.now();
  perfs.push(next - now); // operations
  return [tplClone, perfs];
}

function runScenario(createRowFn,tbodyRef) {
  const numbers = {
    cloning: 0,
    getNodePartNodes: 0,
    accessNodes: 0,
    operations: 0,
    replaceChildren: 0,
  };
  const rows = [];
  for (let i = 0; i < nRows; i++) {
    const [rowEl, perfs] = createRowFn(i);
    rows.push(rowEl);
    numbers["cloning"] += perfs[0];
    numbers["getNodePartNodes"] += perfs[1];
    numbers["accessNodes"] += perfs[2];
    numbers["operations"] += perfs[3];
  }
  const now = performance.now();
  tbodyRef.replaceChildren(...rows);
  numbers["replaceChildren"] = performance.now() - now;
  return numbers;
}

function updateRatio() {
  const baselineVal = Number(document.getElementById("last_baseline_total").value);
  const partsVal = Number(document.getElementById("last_parts_total").value);
  if (baselineVal && partsVal) {
    const logEl = document.getElementById("overall_log");
    if (baselineVal < partsVal) {
      logEl.textContent = `Parts are slower than Manual by ${(100*(partsVal - baselineVal)/baselineVal).toFixed(1)}%`;
    } else {
      logEl.textContent = `Manual is slower than Parts by ${(100*(baselineVal - partsVal)/partsVal).toFixed(1)}%`;
    }
  }
}
function connectButton(button,createRowFn,tbodyRefFn,logIdref) {
  button.addEventListener("click", async () => {
    const numbers = {
      cloning: [],
      getNodePartNodes: [],
      accessNodes: [],
      operations: [],
      replaceChildren: [],
      total: []
    };
    controls.disabled = true;
    await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
    setTimeout(() => {controls.disabled = false;},0);
    for (let i = 0; i < nRuns; i++) {
        resetTable();
        const prev = performance.now();
        const perfs = runScenario(createRowFn,tbodyRefFn());
        numbers["total"].push(performance.now() - prev);
        Object.keys(perfs).forEach((key) => {
          numbers[key].push(perfs[key]);
        });
    }
    Object.keys(numbers).forEach(key => {
      numbers[key] = average(numbers[key]);
    });
    const logEl = document.querySelector(logIdref);
    logEl.textContent = `
cloning: ${(1000 * numbers.cloning / nRows).toFixed(3)} us/call
getNodePartNodes: ${(1000 * numbers.getNodePartNodes / nRows).toFixed(3)} us/call
accessNodes: ${(1000 * numbers.accessNodes / nRows).toFixed(3)} us/call
operations: ${(1000 * numbers.operations / nRows).toFixed(3)} us/call
replaceChildren: ${(numbers.replaceChildren).toFixed(3)} ms/call
total: ${(numbers.total).toFixed(3)} ms (for ${nRows} rows)
`;
    const storageEl = logEl.nextElementSibling;
    storageEl.value = numbers.total;
    updateRatio();
  });
}
connectButton(
  document.querySelector("button#run_baseline"),
  createRowBaseline,
  () => tbodyEl,
  "#baseline_log"
);
connectButton(
  document.querySelector("button#run_parts"),
  createRowParts,
  () => tbodyPart,
  "#parts_log"
);
function average(values) {
  if (values.length === 0) {
    throw new Error("Input array is empty");
  }
  const sum = values.reduce((a, b) => a + b,0);
  return sum / values.length;
}
</script>