1/* 2 * Copyright (C) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { BaseElement, element } from '../../../../../base-ui/BaseElement'; 17import { LitTable } from '../../../../../base-ui/table/lit-table'; 18import { HeapDataInterface } from '../../../../../js-heap/HeapDataInterface'; 19import { 20 ConstructorComparison, 21 ConstructorItem, 22 ConstructorType 23} from '../../../../../js-heap/model/UiStruct'; 24import '../../../../../base-ui/table/lit-table-column'; 25import { TabPaneJsMemoryFilter } from '../TabPaneJsMemoryFilter'; 26import '../TabPaneJsMemoryFilter'; 27import { HeapSnapshotStruct } from '../../../../database/ui-worker/ProcedureWorkerHeapSnapshot'; 28import { LitSelectOption } from '../../../../../base-ui/select/LitSelectOption'; 29import { LitSelect } from '../../../../../base-ui/select/LitSelect'; 30import { TabPaneComparisonHtml } from './TabPaneComparison.html'; 31 32@element('tabpane-comparison') 33export class TabPaneComparison extends BaseElement { 34 private comparisonTableEl: LitTable | undefined | null; 35 private retainerTableEl: LitTable | undefined | null; 36 private comparisonsData: Array<ConstructorComparison> = []; 37 private retainsData: Array<ConstructorItem> = []; 38 private baseFileId: number | undefined | null; 39 private targetFileId: number | undefined | null; 40 private filterEl: TabPaneJsMemoryFilter | undefined | null; 41 private selectEl: LitSelect | undefined | null; 42 private search: HTMLInputElement | undefined | null; 43 private comparisonData: Array<ConstructorItem> = []; 44 private comparisonFilter: Array<ConstructorComparison> = []; 45 private leftArray: Array<ConstructorComparison> = []; 46 private rightArray: Array<ConstructorItem> = []; 47 private rightTheadTable: HTMLDivElement | undefined | null; 48 private leftTheadTable: HTMLDivElement | undefined | null; 49 private comparisonTable: HTMLDivElement | undefined | null; 50 private fileSize: number = 0; 51 52 initElements(): void { 53 this.comparisonTableEl = this.shadowRoot!.querySelector<LitTable>('#tb-comparison') as LitTable; 54 this.retainerTableEl = this.shadowRoot!.querySelector<LitTable>('#tb-retainer') as LitTable; 55 this.filterEl = this.shadowRoot!.querySelector<TabPaneJsMemoryFilter>('#filter'); 56 this.selectEl = this.filterEl?.shadowRoot?.querySelector<LitSelect>('lit-select'); 57 this.search = this.filterEl?.shadowRoot?.querySelector('#js-memory-filter-input') as HTMLInputElement; 58 this.rightTheadTable = this.retainerTableEl!.shadowRoot?.querySelector('.thead') as HTMLDivElement; 59 this.leftTheadTable = this.comparisonTableEl!.shadowRoot?.querySelector('.thead') as HTMLDivElement; 60 this.comparisonTable = this.comparisonTableEl.shadowRoot?.querySelector('.table') as HTMLDivElement; 61 this.classFilter(); 62 } 63 64 initComparison(data: HeapSnapshotStruct, dataListCache: Array<HeapSnapshotStruct>): void { 65 this.clear(); 66 this.retainerTableEl!.snapshotDataSource = []; 67 let fileArr: HeapSnapshotStruct[] = []; 68 let that = this; 69 for (let file of dataListCache) { 70 if (file.id !== data.id) { 71 fileArr.push(file); 72 } 73 } 74 fileArr = fileArr.sort(); 75 this.fileSize = data.size; 76 this.initSelect(data.id, fileArr); 77 this.baseFileId = data.id; 78 this.targetFileId = fileArr[0].id; 79 that.updateComparisonData(data.id, fileArr[0].id); 80 new ResizeObserver(() => { 81 this.comparisonTableEl!.style.height = '100%'; 82 this.comparisonTableEl!.reMeauseHeight(); 83 }).observe(this.parentElement!); 84 } 85 86 updateComparisonData(baseFileId: number, targetFileId: number): void { 87 this.comparisonsData = HeapDataInterface.getInstance().getClassListForComparison(baseFileId, targetFileId); 88 this.comparisonsData.forEach((dataList) => { 89 dataList.objectName = dataList.nodeName; 90 }); 91 if (this.comparisonsData.length > 0) { 92 this.comparisonData = this.comparisonsData; 93 this.comparisonTableEl!.snapshotDataSource = this.comparisonsData; 94 } else { 95 this.comparisonTableEl!.snapshotDataSource = []; 96 } 97 this.comparisonTableEl!.reMeauseHeight(); 98 } 99 100 initSelect(fileId: number, comFileArr: Array<HeapSnapshotStruct>): void { 101 let that = this; 102 let input = this.selectEl!.shadowRoot?.querySelector('input') as HTMLInputElement; 103 this.selectEl!.innerHTML = ''; 104 let option = new LitSelectOption(); 105 option.innerHTML = 'File Name'; 106 option.setAttribute('disabled', 'disabled'); 107 this.selectEl?.appendChild(option); 108 if (comFileArr[0].name) { 109 option.setAttribute('value', comFileArr[0].name); 110 } 111 this.selectEl!.defaultValue = comFileArr[0].name || ''; 112 this.selectEl!.placeholder = comFileArr[0].name || ''; 113 this.selectEl!.dataSource = comFileArr; 114 this.selectEl!.querySelectorAll('lit-select-option').forEach((a) => { 115 a.addEventListener('onSelected', (e) => { 116 this.comparisonTable!.scrollTop = 0; 117 this.retainerTableEl!.snapshotDataSource = []; 118 for (let f of comFileArr) { 119 if (input.value === f.name) { 120 that.updateComparisonData(fileId, f.id); 121 } 122 } 123 e.stopPropagation(); 124 }); 125 }); 126 } 127 128 sortComprisonByColumn(column: string, sort: number): void { 129 switch (sort) { 130 case 0: 131 if (this.search!.value === '') { 132 this.comparisonTableEl!.snapshotDataSource = this.comparisonsData; 133 } else { 134 this.comparisonTableEl!.snapshotDataSource = this.comparisonFilter; 135 } 136 break; 137 default: 138 if (this.search!.value === '') { 139 this.leftArray = [...this.comparisonsData]; 140 } else { 141 this.leftArray = [...this.comparisonFilter]; 142 } 143 this.sortComprisonByColumnExtend(column, sort); 144 break; 145 } 146 } 147 148 private sortComprisonByColumnExtend(column: string, sort: number): void{ 149 switch (column) { 150 case 'addedCount': 151 this.comparisonTableEl!.snapshotDataSource = this.leftArray.sort((a, b) => { 152 return sort === 1 ? a.addedCount - b.addedCount : b.addedCount - a.addedCount; 153 }); 154 break; 155 case 'removedCount': 156 this.comparisonTableEl!.snapshotDataSource = this.leftArray.sort((a, b) => { 157 return sort === 1 ? a.removedCount - b.removedCount : b.removedCount - a.removedCount; 158 }); 159 break; 160 case 'deltaCount': 161 this.comparisonTableEl!.snapshotDataSource = this.leftArray.sort((a, b) => { 162 return sort === 1 ? a.deltaCount - b.deltaCount : b.deltaCount - a.deltaCount; 163 }); 164 break; 165 case 'objectName': 166 this.comparisonTableEl!.snapshotDataSource = this.leftArray.sort((a, b) => { 167 return sort === 1 ? 168 (`${a.objectName }`).localeCompare(`${b.objectName }`) : 169 (`${b.objectName }`).localeCompare(`${a.objectName }`); 170 }); 171 break; 172 case 'addedSize': 173 this.comparisonTableEl!.snapshotDataSource = this.leftArray.sort((a, b) => { 174 return sort === 1 ? a.addedSize - b.addedSize : b.addedSize - a.addedSize; 175 }); 176 break; 177 case 'removedSize': 178 this.comparisonTableEl!.snapshotDataSource = this.leftArray.sort((a, b) => { 179 return sort === 1 ? a.removedSize - b.removedSize : b.removedSize - a.removedSize; 180 }); 181 break; 182 case 'deltaSize': 183 this.comparisonTableEl!.snapshotDataSource = this.leftArray.sort((a, b) => { 184 return sort === 1 ? a.deltaSize - b.deltaSize : b.deltaSize - a.deltaSize; 185 }); 186 break; 187 } 188 } 189 190 sortRetainerByColumn(column: string, sort: number): void { 191 switch (sort) { 192 case 0: 193 this.retainerTableEl!.snapshotDataSource = this.retainsData; 194 break; 195 default: 196 this.rightArray = [...this.retainsData]; 197 switch (column) { 198 case 'distance': 199 this.sortRetainerByDistanceType(sort); 200 break; 201 case 'shallowSize': 202 this.sortRetainerByShallowSizeType(sort); 203 break; 204 case 'retainedSize': 205 this.sortRetainerByRetainedSizeType(sort); 206 break; 207 case 'objectName': 208 this.sortRetainerByObjectNameType(sort); 209 break; 210 } 211 break; 212 } 213 } 214 215 private sortRetainerByObjectNameType(sort: number): void { 216 this.retainerTableEl!.snapshotDataSource = this.rightArray.sort((rightArrA, rightArrB) => { 217 return sort === 1 ? 218 (`${rightArrA.objectName }`).localeCompare(`${rightArrB.objectName }`) : 219 (`${rightArrB.objectName }`).localeCompare(`${rightArrA.objectName }`); 220 }); 221 this.rightArray.forEach((list) => { 222 let retainsTable = (): void => { 223 const getList = (listArr: Array<ConstructorItem>): void => { 224 listArr.sort((listArrA, listArrB) => { 225 return sort === 1 ? 226 (`${listArrA.objectName }`).localeCompare(`${listArrB.objectName }`) : 227 (`${listArrB.objectName }`).localeCompare(`${listArrA.objectName }`); 228 }); 229 listArr.forEach(function (currentRow) { 230 if (currentRow.children.length > 0) { 231 getList(currentRow.children); 232 } 233 }); 234 }; 235 getList(list.children); 236 }; 237 retainsTable(); 238 }); 239 this.retainerTableEl!.snapshotDataSource = this.rightArray; 240 } 241 242 private sortRetainerByRetainedSizeType(sort: number): void { 243 this.retainerTableEl!.snapshotDataSource = this.rightArray.sort((rightArrA, rightArrB) => { 244 return sort === 1 ? 245 rightArrA.retainedSize - rightArrB.retainedSize : 246 rightArrB.retainedSize - rightArrA.retainedSize; 247 }); 248 this.rightArray.forEach((list) => { 249 let retainsTable = (): void => { 250 const getList = (listArr: Array<ConstructorItem>): void => { 251 listArr.sort((listArrA, listArrB) => { 252 return sort === 1 ? 253 listArrA.retainedSize - listArrB.retainedSize : 254 listArrB.retainedSize - listArrA.retainedSize; 255 }); 256 listArr.forEach(function (row) { 257 if (row.children.length > 0) { 258 getList(row.children); 259 } 260 }); 261 }; 262 getList(list.children); 263 }; 264 retainsTable(); 265 }); 266 this.retainerTableEl!.snapshotDataSource = this.rightArray; 267 } 268 269 private sortRetainerByShallowSizeType(sort: number): void { 270 this.retainerTableEl!.snapshotDataSource = this.rightArray.sort((rightArrA, rightArrB) => { 271 return sort === 1 ? 272 rightArrA.shallowSize - rightArrB.shallowSize : 273 rightArrB.shallowSize - rightArrA.shallowSize; 274 }); 275 this.rightArray.forEach((list) => { 276 let retainsTable = (): void => { 277 const getList = (listArr: Array<ConstructorItem>): void => { 278 listArr.sort((listArrA, listArrB) => { 279 return sort === 1 ? 280 listArrA.shallowSize - listArrB.shallowSize : 281 listArrB.shallowSize - listArrA.shallowSize; 282 }); 283 listArr.forEach(function (rowEl) { 284 if (rowEl.children.length > 0) { 285 getList(rowEl.children); 286 } 287 }); 288 }; 289 getList(list.children); 290 }; 291 retainsTable(); 292 }); 293 this.retainerTableEl!.snapshotDataSource = this.rightArray; 294 } 295 296 private sortRetainerByDistanceType(sort: number): void { 297 this.retainerTableEl!.snapshotDataSource = this.rightArray.sort((a, b) => { 298 return sort === 1 ? a.distance - b.distance : b.distance - a.distance; 299 }); 300 this.rightArray.forEach((list) => { 301 let retainsTable = (): void => { 302 const getList = (currentList: Array<ConstructorItem>): void => { 303 currentList.sort((a, b) => { 304 return sort === 1 ? a.distance - b.distance : b.distance - a.distance; 305 }); 306 currentList.forEach(function (currentRow) { 307 if (currentRow.children.length > 0) { 308 getList(currentRow.children); 309 } 310 }); 311 }; 312 getList(list.children); 313 }; 314 retainsTable(); 315 }); 316 this.retainerTableEl!.snapshotDataSource = this.rightArray; 317 } 318 319 classFilter(): void { 320 this.search!.addEventListener('keyup', () => { 321 this.comparisonFilter = []; 322 this.comparisonData.forEach((a: any) => { 323 if (a.objectName.toLowerCase().includes(this.search!.value.toLowerCase())) { 324 this.comparisonFilter.push(a); 325 } else { 326 } 327 }); 328 this.comparisonTableEl!.snapshotDataSource = this.comparisonFilter; 329 let summaryTable = this.comparisonTableEl!.shadowRoot?.querySelector('.table') as HTMLDivElement; 330 summaryTable.scrollTop = 0; 331 this.comparisonTableEl!.reMeauseHeight(); 332 }); 333 } 334 335 clear(): void { 336 this.search!.value = ''; 337 this.rightTheadTable!.removeAttribute('sort'); 338 this.leftTheadTable!.removeAttribute('sort'); 339 this.comparisonTable!.scrollTop = 0; 340 } 341 342 connectedCallback(): void { 343 super.connectedCallback(); 344 let filterHeight = 0; 345 new ResizeObserver(() => { 346 let comparisonPanelFilter = this.shadowRoot!.querySelector('#filter') as HTMLElement; 347 if (comparisonPanelFilter.clientHeight > 0) {filterHeight = comparisonPanelFilter.clientHeight} 348 if (this.parentElement!.clientHeight > filterHeight) { 349 comparisonPanelFilter.style.display = 'flex'; 350 } else { 351 comparisonPanelFilter.style.display = 'none'; 352 } 353 }).observe(this.parentElement!); 354 this.comparisonTableEl!.addEventListener('icon-click', this.comparisonTblIconClickHandler); 355 this.retainerTableEl!.addEventListener('icon-click', this.retainerTblIconClickHandler); 356 this.comparisonTableEl!.addEventListener('column-click', this.comparisonTblColumnClickHandler); 357 this.retainerTableEl!.addEventListener('column-click', this.retainerTblColumnClickHandler); 358 this.comparisonTableEl!.addEventListener('row-click', this.comparisonTblRowClickHandler); 359 this.retainerTableEl!.addEventListener('row-click', this.retainerTblRowClickHandler); 360 } 361 362 disconnectedCallback(): void { 363 super.disconnectedCallback(); 364 this.comparisonTableEl!.removeEventListener('icon-click', this.comparisonTblIconClickHandler); 365 this.retainerTableEl!.removeEventListener('icon-click', this.retainerTblIconClickHandler); 366 this.comparisonTableEl!.removeEventListener('column-click', this.comparisonTblColumnClickHandler); 367 this.retainerTableEl!.removeEventListener('column-click', this.retainerTblColumnClickHandler); 368 this.comparisonTableEl!.removeEventListener('row-click', this.comparisonTblRowClickHandler); 369 this.retainerTableEl!.removeEventListener('row-click', this.retainerTblRowClickHandler); 370 } 371 372 private comparisonTblRowClickHandler = (e: Event): void => { 373 this.rightTheadTable!.removeAttribute('sort'); 374 // @ts-ignore 375 let item = e.detail.data as ConstructorItem; 376 (item as any).isSelected = true; 377 this.retainsData = HeapDataInterface.getInstance().getRetains(item); 378 if (this.retainsData && this.retainsData.length > 0) { 379 this.retainsDataInit(); 380 let i = 0; 381 let that = this; 382 if (this.retainsData[0].distance > 1) { 383 this.retainsData[0].getChildren(); 384 this.retainsData[0].expanded = false; 385 } 386 let retainsTable = (): void => { 387 const getList = (list: Array<ConstructorItem>): void => { 388 list.forEach((structRow) => { 389 let shallow = `${Math.round((structRow.shallowSize / this.fileSize) * 100) }%`; 390 let retained = `${Math.round((structRow.retainedSize / this.fileSize) * 100) }%`; 391 structRow.shallowPercent = shallow; 392 structRow.retainedPercent = retained; 393 let nodeId = `${structRow.nodeName } @${structRow.id}`; 394 structRow.objectName = `${structRow.edgeName }\xa0` + 'in' + `\xa0${ nodeId}`; 395 if (structRow.distance >= 100000000 || structRow.distance === -5) { 396 // @ts-ignore 397 structRow.distance = '-'; 398 } 399 i++; 400 // @ts-ignore 401 if (i < that.retainsData[0].distance - 1 && list[0].distance !== '-') { 402 list[0].getChildren(); 403 list[0].expanded = false; 404 if (structRow.hasNext) { 405 getList(structRow.children); 406 } 407 } else { 408 return; 409 } 410 }); 411 }; 412 getList(that.retainsData[0].children); 413 }; 414 retainsTable(); 415 this.retainerTableEl!.snapshotDataSource = this.retainsData; 416 } else { 417 this.retainerTableEl!.snapshotDataSource = []; 418 } 419 this.resizeObserverObserve(); 420 // @ts-ignore 421 if ((e.detail as any).callBack) { 422 // @ts-ignore 423 (e.detail as any).callBack(true); 424 } 425 }; 426 427 private resizeObserverObserve(){ 428 new ResizeObserver(() => { 429 this.retainerTableEl!.style.height = 'calc(100% - 21px)'; 430 this.retainerTableEl!.reMeauseHeight(); 431 }).observe(this.parentElement!); 432 } 433 434 private retainsDataInit(): void{ 435 this.retainsData.forEach((comparisonRetainEl) => { 436 let shallow = `${Math.round((comparisonRetainEl.shallowSize / this.fileSize) * 100) }%`; 437 let retained = `${Math.round((comparisonRetainEl.retainedSize / this.fileSize) * 100) }%`; 438 comparisonRetainEl.shallowPercent = shallow; 439 comparisonRetainEl.retainedPercent = retained; 440 if (comparisonRetainEl.distance >= 100000000 || comparisonRetainEl.distance === -5) { 441 // @ts-ignore 442 comparisonRetainEl.distance = '-'; 443 } 444 let nodeId = `${comparisonRetainEl.nodeName } @${comparisonRetainEl.id}`; 445 comparisonRetainEl.objectName = `${comparisonRetainEl.edgeName }\xa0` + 'in' + `\xa0${ nodeId}`; 446 }); 447 } 448 449 private retainerTblRowClickHandler = (evt: Event): void => { 450 // @ts-ignore 451 let data = evt.detail.data as ConstructorItem; 452 (data as any).isSelected = true; 453 // @ts-ignore 454 if ((evt.detail as any).callBack) { 455 // @ts-ignore 456 (evt.detail as any).callBack(true); 457 } 458 }; 459 460 private comparisonTblColumnClickHandler = (e: Event): void => { 461 // @ts-ignore 462 this.sortComprisonByColumn(e.detail.key, e.detail.sort); 463 this.comparisonTableEl!.reMeauseHeight(); 464 }; 465 466 private retainerTblColumnClickHandler = (e: Event): void => { 467 // @ts-ignore 468 this.sortRetainerByColumn(e.detail.key, e.detail.sort); 469 this.retainerTableEl!.reMeauseHeight(); 470 }; 471 472 retainerTblIconClickHandler = (e: Event): void => { 473 // @ts-ignore 474 let retainerNext = e.detail.data as ConstructorItem; 475 if (retainerNext) { 476 if (this.retainsData.length > 0) { 477 if (retainerNext.status) { 478 retainerNext.getChildren(); 479 let i = 0; 480 let that = this; 481 let retainsTable = (): void => { 482 const getList = (comList: Array<ConstructorItem>): void => { 483 comList.forEach((row) => { 484 let shallow = `${Math.round((row.shallowSize / this.fileSize) * 100) }%`; 485 let retained = `${Math.round((row.retainedSize / this.fileSize) * 100) }%`; 486 row.shallowPercent = shallow; 487 row.retainedPercent = retained; 488 let nodeId = `${row.nodeName } @${row.id}`; 489 row.objectName = `${row.edgeName }\xa0` + 'in' + `\xa0${ nodeId}`; 490 if (row.distance >= 100000000 || row.distance === -5) { 491 // @ts-ignore 492 row.distance = '-'; 493 } 494 i++; 495 // @ts-ignore 496 if (i < that.retainsData[0].distance - 1 && comList[0].distance !== '-') { 497 comList[0].getChildren(); 498 comList[0].expanded = false; 499 if (row.hasNext) { 500 getList(row.children); 501 } 502 } else { 503 return; 504 } 505 }); 506 }; 507 getList(retainerNext.children); 508 }; 509 retainsTable(); 510 } else { 511 retainerNext.status = true; 512 } 513 if (this.rightTheadTable!.hasAttribute('sort')) { 514 this.retainerTableEl!.snapshotDataSource = this.rightArray; 515 } else { 516 this.retainerTableEl!.snapshotDataSource = this.retainsData; 517 } 518 } else { 519 this.retainerTableEl!.snapshotDataSource = []; 520 } 521 this.resizeObserverObserve(); 522 } 523 }; 524 525 comparisonTblIconClickHandler = (e: Event): void => { 526 // @ts-ignore 527 let clickRow = e.detail.data; 528 if (clickRow.status) { 529 clickRow.targetFileId = this.targetFileId; 530 clickRow.children = HeapDataInterface.getInstance().getNextForComparison(clickRow); 531 if (clickRow.children.length > 0) { 532 for (let item of clickRow.children) { 533 let nodeName = `${item.nodeName } @${item.id}`; 534 item.nodeId = ` @${item.id}`; 535 if (item.isString()) { 536 item.objectName = `"${ item.nodeName}"` + ` @${item.id}`; 537 } else { 538 item.objectName = nodeName; 539 } 540 item.deltaCount = '-'; 541 item.deltaSize = '-'; 542 if (item.edgeName !== '') { 543 item.objectName = `${item.edgeName }\xa0` + '::' + `\xa0${ nodeName}`; 544 } else { 545 if (item.fileId === this.baseFileId) { 546 item.addedCount = '•'; 547 item.addedSize = item.shallowSize; 548 item.removedCount = '-'; 549 item.removedSize = '-'; 550 } else if (item.fileId) { 551 item.removedCount = '•'; 552 item.removedSize = item.shallowSize; 553 item.addedCount = '-'; 554 item.addedSize = '-'; 555 } 556 } 557 if (item.type === ConstructorType.FiledType) { 558 item.removedCount = '-'; 559 item.removedSize = '-'; 560 item.addedCount = '-'; 561 item.addedSize = '-'; 562 } 563 } 564 } else { 565 this.comparisonTableEl!.snapshotDataSource = []; 566 } 567 } else { 568 clickRow.status = true; 569 } 570 this.comparisonTblIconClickData(); 571 }; 572 573 private comparisonTblIconClickData(): void{ 574 if (this.search!.value !== '') { 575 if (this.leftTheadTable!.hasAttribute('sort')) { 576 this.comparisonTableEl!.snapshotDataSource = this.leftArray; 577 } else { 578 this.comparisonTableEl!.snapshotDataSource = this.comparisonFilter; 579 } 580 } else { 581 if (this.leftTheadTable!.hasAttribute('sort')) { 582 this.comparisonTableEl!.snapshotDataSource = this.leftArray; 583 } else { 584 this.comparisonTableEl!.snapshotDataSource = this.comparisonsData; 585 } 586 } 587 new ResizeObserver(() => { 588 this.comparisonTableEl!.style.height = '100%'; 589 this.comparisonTableEl!.reMeauseHeight(); 590 }).observe(this.parentElement!); 591 } 592 593 initHtml(): string { 594 return TabPaneComparisonHtml; 595 } 596} 597