github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/tool/data/lsm.js (about) 1 // TODO(peter): 2 // 3 // - interactions 4 // - mouse wheel: horizontal zoom 5 // - click/drag: horizontal pan 6 7 "use strict"; 8 9 // The heights of each level. The first few levels are given smaller 10 // heights to account for the increasing target file size. 11 // 12 // TODO(peter): Use the TargetFileSizes specified in the OPTIONS file. 13 let levelHeights = [16, 16, 16, 16, 32, 64, 128]; 14 const offsetStart = 24; 15 let levelOffsets = generateLevelOffsets(); 16 const lineStart = 105; 17 const sublevelHeight = 16; 18 let levelWidth = 0; 19 20 { 21 // Create the base DOM elements. 22 let c = d3 23 .select("body") 24 .append("div") 25 .attr("id", "container"); 26 let h = c.append("div").attr("id", "header"); 27 h 28 .append("div") 29 .attr("id", "index-container") 30 .append("input") 31 .attr("type", "text") 32 .attr("id", "index") 33 .attr("autocomplete", "off"); 34 let checkboxContainer = h 35 .append("div") 36 .attr("id", "checkbox-container"); 37 checkboxContainer.append("input") 38 .attr("type", "checkbox") 39 .attr("id", "flatten-sublevels") 40 .on("change", () => {version.onCheckboxChange(d3.event.target.checked)}); 41 checkboxContainer.append("label") 42 .attr("for", "flatten-sublevels") 43 .text("Show sublevels"); 44 h.append("svg").attr("id", "slider"); 45 c.append("svg").attr("id", "vis"); 46 } 47 48 let vis = d3.select("#vis"); 49 50 function renderHelp() { 51 vis 52 .append("text") 53 .attr("class", "help") 54 .attr("x", 10) 55 .attr("y", levelOffsets[6] + 30) 56 .text( 57 "(space: start/stop, left-arrow[+shift]: step-back, right-arrow[+shift]: step-forward)" 58 ); 59 } 60 61 function renderReason() { 62 return vis 63 .append("text") 64 .attr("class", "reason") 65 .attr("x", 10) 66 .attr("y", 16); 67 } 68 69 let reason = renderReason(); 70 71 let index = d3.select("#index"); 72 73 // Pretty formatting of a number in human readable units. 74 function humanize(s) { 75 const iecSuffixes = [" B", " KB", " MB", " GB", " TB", " PB", " EB"]; 76 if (s < 10) { 77 return "" + s; 78 } 79 let e = Math.floor(Math.log(s) / Math.log(1024)); 80 let suffix = iecSuffixes[Math.floor(e)]; 81 let val = Math.floor(s / Math.pow(1024, e) * 10 + 0.5) / 10; 82 return val.toFixed(val < 10 ? 1 : 0) + suffix; 83 } 84 85 function generateLevelOffsets() { 86 return levelHeights.map((v, i) => 87 levelHeights.slice(0, i + 1).reduce((sum, elem) => sum + elem, offsetStart) 88 ); 89 } 90 91 function styleWidth(e) { 92 let width = +e.style("width").slice(0, -2); 93 return Math.round(Number(width)); 94 } 95 96 function styleHeight(e) { 97 let height = +e.style("height").slice(0, -2); 98 return Math.round(Number(height)); 99 } 100 101 let sliderX, sliderHandle; 102 let offsetSliderX; 103 104 // The version object holds the current LSM state. 105 let version = { 106 levels: [[], [], [], [], [], [], []], 107 sublevels: [], 108 numSublevels: 0, 109 showSublevels: false, 110 // Generated after every change using setLevelsInfo(). 111 levelsInfo: [], 112 // The version edit index. 113 index: -1, 114 115 init: function() { 116 for (let edit of data.Edits) { 117 if (edit.Sublevels === null || edit.Sublevels === undefined) { 118 continue; 119 } 120 for (let [file, sublevel] of Object.entries(edit.Sublevels)) { 121 if (sublevel >= this.numSublevels) { 122 this.numSublevels = sublevel + 1; 123 } 124 } 125 } 126 for (let i = 0; i < this.numSublevels; i++) { 127 this.sublevels.push([]); 128 } 129 d3.select("#checkbox-container label") 130 .text("Show sublevels (" + this.numSublevels.toString() + ")"); 131 this.setHeights(); 132 this.setLevelsInfo(); 133 renderHelp(); 134 }, 135 136 setHeights: function() { 137 // Update the height of level 0 to account for the number of sublevels, 138 // if there are any. 139 if (this.numSublevels > 0 && this.showSublevels === true) { 140 levelHeights[0] = sublevelHeight * this.numSublevels; 141 } else { 142 levelHeights[0] = sublevelHeight; 143 } 144 levelOffsets = generateLevelOffsets(); 145 vis.style("height", levelOffsets[6] + 100); 146 }, 147 148 onCheckboxChange: function(value) { 149 this.showSublevels = value; 150 vis.selectAll("*") 151 .remove(); 152 reason = renderReason(); 153 this.setHeights(); 154 this.setLevelsInfo(); 155 renderHelp(); 156 157 this.render(true); 158 this.updateSize(); 159 }, 160 161 // Set the version edit index. This steps either forward or 162 // backward through the version edits, applying or unapplying each 163 // edit. 164 set: function(index) { 165 let prevIndex = this.index; 166 if (index < 0) { 167 index = 0; 168 } else if (index >= data.Edits.length) { 169 index = data.Edits.length - 1; 170 } 171 if (index == this.index) { 172 return; 173 } 174 175 // If the current edit index is less than the target index, 176 // step forward applying edits. 177 for (; this.index < index; this.index++) { 178 let edit = data.Edits[this.index + 1]; 179 for (let level in edit.Deleted) { 180 this.remove(level, edit.Deleted[level]); 181 } 182 for (let level in edit.Added) { 183 this.add(level, edit.Added[level]); 184 } 185 } 186 187 // If the current edit index is greater than the target index, 188 // step backward unapplying edits. 189 for (; this.index > index; this.index--) { 190 let edit = data.Edits[this.index]; 191 for (let level in edit.Added) { 192 this.remove(level, edit.Added[level]); 193 } 194 for (let level in edit.Deleted) { 195 this.add(level, edit.Deleted[level]); 196 } 197 } 198 199 // Build the sublevels from this.levels[0]. They need to be rebuilt from 200 // scratch each time there's a change to L0. 201 this.sublevels = []; 202 while(this.sublevels.length < this.numSublevels) { 203 this.sublevels.push([]); 204 } 205 for (let file of this.levels[0]) { 206 let sublevel = null; 207 for (let i = index; i >= 0 && (sublevel === null || sublevel === undefined); i--) { 208 if (data.Edits[i].Sublevels == null || data.Edits[i].Sublevels == undefined) { 209 continue; 210 } 211 sublevel = data.Edits[i].Sublevels[file]; 212 } 213 this.sublevels[sublevel].push(file); 214 } 215 216 // Sort the levels. 217 for (let i in this.levels) { 218 if (i == 0) { 219 for (let j in this.sublevels) { 220 this.sublevels[j].sort(function(a, b) { 221 let fa = data.Files[a]; 222 let fb = data.Files[b]; 223 if (fa.Smallest < fb.Smallest) { 224 return -1; 225 } 226 if (fa.Smallest > fb.Smallest) { 227 return +1; 228 } 229 return 0; 230 }); 231 } 232 this.levels[i].sort(function(a, b) { 233 let fa = data.Files[a]; 234 let fb = data.Files[b]; 235 if (fa.LargestSeqNum < fb.LargestSeqNum) { 236 return -1; 237 } 238 if (fa.LargestSeqNum > fb.LargestSeqNum) { 239 return +1; 240 } 241 if (fa.SmallestSeqNum < fb.SmallestSeqNum) { 242 return -1; 243 } 244 if (fa.SmallestSeqNum > fb.SmallestSeqNum) { 245 return +1; 246 } 247 return a < b; 248 }); 249 } else { 250 this.levels[i].sort(function(a, b) { 251 let fa = data.Files[a]; 252 let fb = data.Files[b]; 253 if (fa.Smallest < fb.Smallest) { 254 return -1; 255 } 256 if (fa.Smallest > fb.Smallest) { 257 return +1; 258 } 259 return 0; 260 }); 261 } 262 } 263 264 this.updateLevelsInfo(); 265 this.render(prevIndex === -1); 266 }, 267 268 // Add the specified sstables to the specifed level. 269 add: function(level, fileNums) { 270 for (let i = 0; i < fileNums.length; i++) { 271 this.levels[level].push(fileNums[i]); 272 } 273 }, 274 275 // Remove the specified sstables from the specifed level. 276 remove: function(level, fileNums) { 277 let l = this.levels[level]; 278 for (let i = 0; i < l.length; i++) { 279 if (fileNums.indexOf(l[i]) != -1) { 280 l[i] = l[l.length - 1]; 281 l.pop(); 282 i--; 283 } 284 } 285 }, 286 287 // Return the size of the sstables in a level. 288 size: function(level, sublevel) { 289 if (level == 0 && sublevel !== null && sublevel !== undefined) { 290 return this.sublevels[sublevel].reduce( 291 (sum, elem) => sum + data.Files[elem].Size, 292 0 293 ); 294 } 295 return (this.levels[level] || []).reduce( 296 (sum, elem) => sum + data.Files[elem].Size, 297 0 298 ); 299 }, 300 301 // Returns the height to use for an sstable. 302 height: function(fileNum) { 303 let meta = data.Files[fileNum]; 304 return Math.ceil((meta.Size + 1024.0 * 1024.0 - 1) / (1024.0 * 1024.0)); 305 }, 306 307 scale: function(level) { 308 return levelWidth < this.levelsInfo[level].files.length 309 ? levelWidth / this.levelsInfo[level].files.length 310 : 1; 311 }, 312 313 // Return a summary of the count and size of the specified sstables. 314 summarize: function(level, fileNums) { 315 let count = 0; 316 let size = 0; 317 for (let fileNum of fileNums) { 318 count++; 319 size += data.Files[fileNum].Size; 320 } 321 return count + " @ " + "L" + level + " (" + humanize(size) + ")"; 322 }, 323 324 // Return a textual description of a version edit. 325 describe: function(edit) { 326 let s = edit.Reason; 327 328 if (edit.Deleted) { 329 let sep = " "; 330 for (let i = 0; i < 7; i++) { 331 if (edit.Deleted[i]) { 332 s += sep + this.summarize(i, edit.Deleted[i]); 333 sep = " + "; 334 } 335 } 336 } 337 338 if (edit.Added) { 339 let sep = " => "; 340 for (let i = 0; i < 7; i++) { 341 if (edit.Added[i]) { 342 s += sep + this.summarize(i, edit.Added[i]); 343 sep = " + "; 344 } 345 } 346 } 347 348 return s; 349 }, 350 351 setLevelsInfo: function() { 352 let sublevelCount = this.numSublevels; 353 let levelsInfo = []; 354 let levelsStart = 1; 355 if (this.showSublevels === true) { 356 levelsInfo = this.sublevels.map((files, sublevel) => ({ 357 files: files, 358 levelString: "L0." + sublevel.toString(), 359 levelDisplayString: (sublevel === this.numSublevels - 1 ? 360 "L0." : " .") + sublevel.toString(), 361 levelClass: "L0-" + sublevel.toString(), 362 level: 0, 363 offset: offsetStart + (sublevelHeight * (sublevelCount - sublevel)), 364 height: sublevelHeight, 365 size: humanize(this.size(0, sublevel)), 366 })); 367 if (levelsInfo.length === 0) { 368 levelsStart = 0; 369 } 370 levelsInfo.reverse(); 371 } else { 372 levelsStart = 0; 373 } 374 375 levelsInfo = levelsInfo.concat(this.levels.slice(levelsStart).map((files, level) => ({ 376 files: files, 377 levelString: "L" + (level+levelsStart).toString(), 378 levelDisplayString: "L" + (level+levelsStart).toString(), 379 levelClass: "L" + (level+levelsStart).toString(), 380 level: level, 381 offset: levelOffsets[level+levelsStart], 382 height: levelHeights[level+levelsStart], 383 size: humanize(this.size(level+levelsStart)), 384 }))); 385 this.levelsInfo = levelsInfo; 386 }, 387 388 updateLevelsInfo: function() { 389 let levelsStart = 1; 390 if (this.showSublevels === true) { 391 this.sublevels.forEach((files, sublevel) => { 392 this.levelsInfo[this.numSublevels - (sublevel + 1)].files = files; 393 this.levelsInfo[this.numSublevels - (sublevel + 1)].size = humanize(this.size(0, sublevel)); 394 }); 395 if (this.numSublevels === 0) { 396 levelsStart = 0; 397 } 398 } else { 399 levelsStart = 0; 400 } 401 402 this.levels.slice(levelsStart).forEach((files, level) => { 403 let sublevelOffset = this.showSublevels === true ? this.numSublevels : 0; 404 this.levelsInfo[sublevelOffset + level].files = files; 405 this.levelsInfo[sublevelOffset + level].size = humanize(this.size(levelsStart + level)); 406 }); 407 }, 408 409 render: function(redraw) { 410 let version = this; 411 412 vis.interrupt(); 413 414 // Render the edit info. 415 let info = "[" + this.describe(data.Edits[this.index]) + "]"; 416 reason.text(info); 417 418 // Render the text for each level: sstable count and size. 419 vis 420 .selectAll("text.levels") 421 .data(this.levelsInfo) 422 .enter() 423 .append("text") 424 .attr("class", "levels") 425 .attr("x", 10) 426 .attr("y", d => d.offset) 427 .html(d => d.levelDisplayString); 428 vis 429 .selectAll("text.counts") 430 .data(this.levelsInfo) 431 .text((d, i) => d.files.length) 432 .enter() 433 .append("text") 434 .attr("class", "counts") 435 .attr("text-anchor", "end") 436 .attr("x", 55) 437 .attr("y", d => d.offset) 438 .text(d => d.files.length); 439 vis 440 .selectAll("text.sizes") 441 .data(this.levelsInfo) 442 .text((d, i) => d.size) 443 .enter() 444 .append("text") 445 .attr("class", "sizes") 446 .attr("text-anchor", "end") 447 .attr("x", 100) 448 .attr("y", (d, i) => d.offset) 449 .text(d => d.size); 450 451 // Render each of the levels. Each level is composed of an 452 // outer group which provides a clipping recentangle, an inner 453 // group defining the coordinate system, an overlap rectangle 454 // to capture mouse events, an indicator rectangle used to 455 // display sstable overlaps, and the per-sstable rectangles. 456 for (let i in this.levelsInfo) { 457 let g, clipG; 458 if (redraw === false) { 459 g = vis 460 .selectAll("g.clip" + this.levelsInfo[i].levelClass) 461 .select("g") 462 .data([i]); 463 clipG = g 464 .enter() 465 .append("g") 466 .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass) 467 .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")"); 468 } else { 469 clipG = vis 470 .append("g") 471 .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass) 472 .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")") 473 .data([i]); 474 g = clipG 475 .append("g"); 476 } 477 clipG 478 .append("g") 479 .attr( 480 "transform", 481 "translate(" + 482 lineStart + 483 "," + 484 this.levelsInfo[i].offset + 485 ") scale(1,-1)" 486 ); 487 clipG.append("rect").attr("class", "indicator"); 488 489 // Define the overlap rectangle for capturing mouse events. 490 clipG 491 .append("rect") 492 .attr("x", lineStart) 493 .attr("y", this.levelsInfo[i].offset - this.levelsInfo[i].height) 494 .attr("width", levelWidth) 495 .attr("height", this.levelsInfo[i].height) 496 .attr("opacity", 0) 497 .attr("pointer-events", "all") 498 .on("mousemove", i => version.onMouseMove(i)) 499 .on("mouseout", function() { 500 reason.text(info); 501 vis.selectAll("rect.indicator").attr("fill", "none"); 502 }); 503 504 // Scale each level to fit within the display. 505 let s = this.scale(i); 506 g.attr( 507 "transform", 508 "translate(" + 509 lineStart + 510 "," + 511 this.levelsInfo[i].offset + 512 ") scale(" + 513 s + 514 "," + 515 -(1 / s) + 516 ")" 517 ); 518 519 // Render the sstables for the level. 520 let level = g.selectAll("rect." + this.levelsInfo[i].levelClass).data(this.levelsInfo[i].files); 521 level.attr("fill", fileNum => (data.Files[fileNum].Virtual?"#8A9":"#555")).attr("x", (fileNum, i) => i); 522 level 523 .enter() 524 .append("rect") 525 .attr("class", this.levelsInfo[i].levelClass + " sstable") 526 .attr("id", fileNum => fileNum) 527 .attr("fill", fileNum => (data.Files[fileNum].Virtual?"orange":"red")) 528 .attr("x", (fileNum, i) => i) 529 .attr("y", 0) 530 .attr("width", 1) 531 .attr("height", fileNum => version.height(fileNum)); 532 level.exit().remove(); 533 } 534 535 sliderHandle.attr("cx", sliderX(version.index)); 536 index.node().value = version.index + data.StartEdit; 537 }, 538 539 onMouseMove: function(i) { 540 i = Number(i); 541 if (Number.isNaN(i) || i >= this.levelsInfo.length || this.levelsInfo[i].files.length === 0) { 542 return; 543 } 544 545 // The mouse coordinates are relative to the 546 // SVG element. Adjust to be relative to the 547 // level position. 548 let mousex = d3.mouse(vis.node())[0] - lineStart; 549 let index = Math.round(mousex / this.scale(i)); 550 if (index < 0) { 551 index = 0; 552 } else if (index >= this.levelsInfo[i].files.length) { 553 index = this.levelsInfo[i].files.length - 1; 554 } 555 let fileNum = this.levelsInfo[i].files[index]; 556 let meta = data.Files[fileNum]; 557 558 // Find the start and end index of the tables 559 // that overlap with filenum. 560 let overlapInfo = ""; 561 for (let j = 1; j < this.levelsInfo.length; j++) { 562 if (this.levelsInfo[i].files.length === 0) { 563 continue; 564 } 565 let indicator = vis.select("g.clip" + this.levelsInfo[j].levelClass + " rect.indicator"); 566 indicator 567 .attr("fill", "black") 568 .attr("opacity", 0.3) 569 .attr("y", this.levelsInfo[j].offset - this.levelsInfo[j].height) 570 .attr("height", this.levelsInfo[j].height); 571 if (j === i) { 572 continue; 573 } 574 let fileNums = this.levelsInfo[j].files; 575 for (let k in fileNums) { 576 let other = data.Files[fileNums[k]]; 577 if (other.Largest < meta.Smallest) { 578 continue; 579 } 580 let s = this.scale(j); 581 let t = k; 582 for (; k < fileNums.length; k++) { 583 let other = data.Files[fileNums[k]]; 584 if (other.Smallest >= meta.Largest) { 585 break; 586 } 587 } 588 if (k === t) { 589 indicator.attr("x", lineStart + s * t).attr("width", s); 590 } else { 591 indicator 592 .attr("x", lineStart + s * t) 593 .attr("width", Math.max(0.5, s * (k - t))); 594 } 595 if (i + 1 === j && k > t) { 596 let overlapSize = this.levelsInfo[j].files 597 .slice(t, k) 598 .reduce((sum, elem) => sum + data.Files[elem].Size, 0); 599 600 overlapInfo = 601 " overlaps " + 602 (k - t) + 603 " @ " + 604 this.levelsInfo[j].levelString + 605 " (" + 606 humanize(overlapSize) + 607 ")"; 608 } 609 break; 610 } 611 } 612 613 reason.text( 614 "[" + 615 this.levelsInfo[i].levelString + 616 (data.Files[fileNum].Virtual? " v":" ") + 617 fileNum + 618 " (" + 619 humanize(data.Files[fileNum].Size) + 620 ")" + 621 overlapInfo + 622 " <" + 623 data.Keys[data.Files[fileNum].Smallest].Pretty + 624 ", " + 625 data.Keys[data.Files[fileNum].Largest].Pretty + 626 ">" + 627 "]" 628 ); 629 630 vis 631 .select("g.clip" + this.levelsInfo[i].levelClass + " rect.indicator") 632 .attr("x", lineStart + this.scale(i) * index) 633 .attr("width", 1); 634 }, 635 636 // Recalculate structures related to the page width. 637 updateSize: function() { 638 let svg = d3.select("#slider").html(""); 639 640 let margin = { right: 10, left: 10 }; 641 642 let width = styleWidth(d3.select("#slider")) - margin.left - margin.right, 643 height = styleHeight(svg); 644 645 sliderX = d3 646 .scaleLinear() 647 .domain([0, data.Edits.length - 1]) 648 .range([0, width]) 649 .clamp(true); 650 651 // Used only to generate offset ticks for slider. 652 // sliderX is used to index into the data.Edits array (0-indexed). 653 offsetSliderX = d3 654 .scaleLinear() 655 .domain([data.StartEdit, data.StartEdit + data.Edits.length - 1]) 656 .range([0, width]); 657 658 let slider = svg 659 .append("g") 660 .attr("class", "slider") 661 .attr("transform", "translate(" + margin.left + "," + height / 2 + ")"); 662 663 slider 664 .append("line") 665 .attr("class", "track") 666 .attr("x1", sliderX.range()[0]) 667 .attr("x2", sliderX.range()[1]) 668 .select(function() { 669 return this.parentNode.appendChild(this.cloneNode(true)); 670 }) 671 .attr("class", "track-inset") 672 .select(function() { 673 return this.parentNode.appendChild(this.cloneNode(true)); 674 }) 675 .attr("class", "track-overlay") 676 .call( 677 d3 678 .drag() 679 .on("start.interrupt", function() { 680 slider.interrupt(); 681 }) 682 .on("start drag", function() { 683 version.set(Math.round(sliderX.invert(d3.event.x))); 684 }) 685 ); 686 687 slider 688 .insert("g", ".track-overlay") 689 .attr("class", "ticks") 690 .attr("transform", "translate(0," + 18 + ")") 691 .selectAll("text") 692 .data(offsetSliderX.ticks(10)) 693 .enter() 694 .append("text") 695 .attr("x", offsetSliderX) 696 .attr("text-anchor", "middle") 697 .text(function(d) { 698 return d; 699 }); 700 701 sliderHandle = slider 702 .insert("circle", ".track-overlay") 703 .attr("class", "handle") 704 .attr("r", 9) 705 .attr("cx", sliderX(version.index)); 706 707 levelWidth = styleWidth(vis) - 10 - lineStart; 708 let lineEnd = lineStart + levelWidth; 709 710 vis 711 .selectAll("line") 712 .data(this.levelsInfo) 713 .attr("x2", lineEnd) 714 .enter() 715 .append("line") 716 .attr("x1", lineStart) 717 .attr("x2", lineEnd) 718 .attr("y1", d => d.offset) 719 .attr("y2", d => d.offset) 720 .attr("stroke", "#ddd"); 721 722 vis 723 .selectAll("defs clipPath rect") 724 .data(this.levelsInfo) 725 .attr("width", lineEnd - lineStart) 726 .enter() 727 .append("defs") 728 .append("clipPath") 729 .attr("id", d => d.levelClass) 730 .append("rect") 731 .attr("x", lineStart) 732 .attr("y", d => d.offset - d.height) 733 .attr("width", lineEnd - lineStart) 734 .attr("height", d => d.height); 735 }, 736 }; 737 738 window.onload = function() { 739 version.init(); 740 version.updateSize(); 741 version.set(0); 742 }; 743 744 window.addEventListener("resize", function() { 745 version.updateSize(); 746 version.render(); 747 }); 748 749 let timer; 750 751 function startPlayback(increment) { 752 timer = d3.timer(function() { 753 let lastIndex = version.index; 754 version.set(version.index + increment); 755 if (lastIndex == version.index) { 756 timer.stop(); 757 timer = null; 758 } 759 }); 760 } 761 762 function stopPlayback() { 763 if (timer == null) { 764 return false; 765 } 766 timer.stop(); 767 timer = null; 768 return true; 769 } 770 771 document.addEventListener("keydown", function(e) { 772 switch (e.keyCode) { 773 case 37: // left arrow 774 stopPlayback(); 775 version.set(version.index - (e.shiftKey ? 10 : 1)); 776 return; 777 case 39: // right arrow 778 stopPlayback(); 779 version.set(version.index + (e.shiftKey ? 10 : 1)); 780 return; 781 case 32: // space 782 if (stopPlayback()) { 783 return; 784 } 785 startPlayback(1); 786 return; 787 } 788 }); 789 790 index.on("input", function() { 791 if (!isNaN(+this.value)) { 792 const val = Number(this.value) - data.StartEdit; 793 if (val >= 0) { 794 version.set(val); 795 } 796 } 797 });