1 <!DOCTYPE HTML> 2 <html> 3 <!-- 4 Copyright (c) 2012 The Chromium Authors. All rights reserved. 5 Use of this source code is governed by a BSD-style license that can be 6 found in the LICENSE file. 7 --> 8 <head i18n-values="dir:textdirection;"> 9 <title>TimelineAnalysis tests</title> 10 <script src="base.js"></script> 11 <style> 12 .timeline-view { 13 border: 1px solid black; 14 margin: 10px; 15 } 16 .timeline-find-dialog { 17 border: 1px solid black; 18 margin: 10px; 19 } 20 </style> 21 </head> 22 <body> 23 <script> 24 base.require('unittest'); 25 base.require('tracing_controller'); 26 base.require('test_utils'); 27 base.require('timeline_model'); 28 base.require('trace_event_importer'); 29 base.require('timeline_analysis'); 30 base.require('timeline_filter'); 31 base.require('tracks.timeline_counter_track'); 32 base.require('tracks.timeline_slice_track'); 33 base.require('tracks.timeline_thread_track'); 34 base.require('timeline'); /* TODO(nduca): reduce dependency */ 35 </script> 36 <script> 37 'use strict'; 38 39 var TimelineCounter = tracing.TimelineCounter; 40 var TimelineThread = tracing.TimelineThread; 41 var TimelineAnalysisView = tracing.TimelineAnalysisView; 42 var TimelineModel = tracing.TimelineModel; 43 var TimelineThreadTrack = tracks.TimelineThreadTrack; 44 var TimelineSelection = tracing.TimelineSelection; 45 var TimelineTitleFilter = tracing.TimelineTitleFilter; 46 var TimelineCounterTrack = tracks.TimelineCounterTrack; 47 var TimelineSliceTrack = tracks.TimelineSliceTrack; 48 var newSliceNamed = test_utils.newSliceNamed; 49 var newSliceCategory = test_utils.newSliceCategory; 50 51 function createSelectionWithSingleSlice(withCategory) { 52 var model = new TimelineModel(); 53 var t53 = model.getOrCreateProcess(52).getOrCreateThread(53); 54 if (withCategory) 55 t53.pushSlice(newSliceCategory('foo', 'b', 0, 0.002)); 56 else 57 t53.pushSlice(newSliceNamed('b', 0, 0.002)); 58 59 var t53track = new TimelineThreadTrack(); 60 t53track.thread = t53; 61 62 var selection = new TimelineSelection(); 63 t53track.addAllObjectsMatchingFilterToSelection( 64 new TimelineTitleFilter('b'), selection); 65 assertEquals(1, selection.length); 66 67 return selection; 68 } 69 70 function createSelectionWithTwoSlices() { 71 var events = [ 72 {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}, 73 {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}, 74 {name: 'aa', args: {}, pid: 52, ts: 640, cat: 'foo', tid: 53, ph: 'B'}, 75 {name: 'aa', args: {}, pid: 52, ts: 700, cat: 'foo', tid: 53, ph: 'E'} 76 ]; 77 var model = new TimelineModel(events); 78 79 var t53track = new TimelineThreadTrack(); 80 t53track.thread = model.processes[52].threads[53]; 81 82 var selection = new TimelineSelection(); 83 t53track.addAllObjectsMatchingFilterToSelection( 84 new TimelineTitleFilter('a'), selection); 85 assertEquals(2, selection.length); 86 87 return selection; 88 } 89 90 function createSelectionWithTwoSlicesSameTitle() { 91 var events = [ 92 {name: 'c', args: {}, pid: 52, ts: 620, cat: 'foo', tid: 53, ph: 'B'}, 93 {name: 'c', args: {}, pid: 52, ts: 660, cat: 'foo', tid: 53, ph: 'E'}, 94 {name: 'c', args: {}, pid: 52, ts: 740, cat: 'foo', tid: 53, ph: 'B'}, 95 {name: 'c', args: {}, pid: 52, ts: 800, cat: 'foo', tid: 53, ph: 'E'} 96 ]; 97 var model = new TimelineModel(events); 98 99 var t53track = new TimelineThreadTrack(); 100 t53track.thread = model.processes[52].threads[53]; 101 102 var selection = new TimelineSelection(); 103 t53track.addAllObjectsMatchingFilterToSelection( 104 new TimelineTitleFilter('c'), selection); 105 assertEquals(2, selection.length); 106 107 return selection; 108 } 109 110 function createSelectionWithCounters(numSamples) { 111 if (numSamples > 2 || numSamples < 1) 112 throw new Error('This function only supports 1 or 2 samples'); 113 var events = [ 114 {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1, 115 ph: 'C', id: 0}, 116 {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1, 117 ph: 'C', id: 0} 118 ]; 119 var model = new TimelineModel(events); 120 var p = model.processes[1]; 121 var ctr = model.processes[1].counters['foo.ctr[0]']; 122 assertEquals('ctr[0]', ctr.name); 123 assertEquals(2, ctr.numSamples); 124 assertEquals(1, ctr.numSeries); 125 assertArrayEquals([0, 0.01], ctr.timestamps); 126 assertArrayEquals([0, 10], ctr.samples); 127 128 var selection = new TimelineSelection(); 129 var t1track = new TimelineThreadTrack(); 130 selection.addCounterSample(t1track, ctr, 1); 131 132 if (numSamples == 1) 133 return selection; 134 135 selection.addCounterSample(t1track, ctr, 0); 136 return selection; 137 } 138 139 function createSelectionWithTwoSeriesSingleCounter() { 140 var events = [ 141 {name: 'ctr', args: {'bytesallocated': 0, 'bytesfree': 25}, pid: 1, 142 ts: 0, cat: 'foo', tid: 1, ph: 'C', id: 0}, 143 {name: 'ctr', args: {'bytesallocated': 10, 'bytesfree': 15}, pid: 1, 144 ts: 10, cat: 'foo', tid: 1, ph: 'C', id: 0}, 145 {name: 'ctr', args: {'bytesallocated': 20, 'bytesfree': 5}, pid: 1, 146 ts: 20, cat: 'foo', tid: 1, ph: 'C', id: 0} 147 ]; 148 var model = new TimelineModel(events); 149 var p = model.processes[1]; 150 var ctr = model.processes[1].counters['foo.ctr[0]']; 151 assertEquals('ctr[0]', ctr.name); 152 assertEquals(3, ctr.numSamples); 153 assertEquals(2, ctr.numSeries); 154 155 var selection = new TimelineSelection(); 156 var t1track = new TimelineThreadTrack(); 157 selection.addCounterSample(t1track, ctr, 1); 158 159 return selection; 160 } 161 162 function createSelectionWithTwoSeriesTwoCounters() { 163 var ctr1 = new TimelineCounter(null, 0, '', 'ctr1'); 164 ctr1.seriesNames.push('bytesallocated', 'bytesfree'); 165 ctr1.seriesColors.push(0, 1); 166 ctr1.timestamps.push(0, 10, 20); 167 ctr1.samples.push(0, 25, 10, 15, 20, 5); 168 169 var ctr2 = new TimelineCounter(null, 0, '', 'ctr2'); 170 ctr2.seriesNames.push('bytesallocated', 'bytesfree'); 171 ctr2.seriesColors.push(0, 1); 172 ctr2.timestamps.push(0, 10, 20); 173 ctr2.samples.push(0, 25, 10, 15, 20, 5); 174 175 var selection = new TimelineSelection(); 176 var t1track = new TimelineThreadTrack(); 177 selection.addCounterSample(t1track, ctr1, 1); 178 selection.addCounterSample(t1track, ctr2, 2); 179 180 return selection; 181 } 182 183 function createSelectionWithTwoCountersDiffSeriesDiffHits() { 184 var ctr1 = new TimelineCounter(null, 0, '', 'a'); 185 ctr1.seriesNames.push('bytesallocated'); 186 ctr1.seriesColors.push(0); 187 ctr1.timestamps.push(0, 10, 20); 188 ctr1.samples.push(0, 25, 10); 189 assertEquals('a', ctr1.name); 190 assertEquals(3, ctr1.numSamples); 191 assertEquals(1, ctr1.numSeries); 192 193 var ctr2 = new TimelineCounter(null, 0, '', 'b'); 194 ctr2.seriesNames.push('bytesallocated', 'bytesfree'); 195 ctr2.seriesColors.push(0, 1); 196 ctr2.timestamps.push(0, 10, 20, 30); 197 ctr2.samples.push(0, 25, 10, 15, 20, 5, 25, 0); 198 assertEquals('b', ctr2.name); 199 assertEquals(4, ctr2.numSamples); 200 assertEquals(2, ctr2.numSeries); 201 202 var selection = new TimelineSelection(); 203 var t1track = new TimelineThreadTrack(); 204 selection.addCounterSample(t1track, ctr1, 1); 205 selection.addCounterSample(t1track, ctr2, 2); 206 207 return selection; 208 } 209 210 function createSelectionWithSingleSliceSingleCounter() { 211 var model = new TimelineModel(); 212 var thread = model.getOrCreateProcess(1).getOrCreateThread(1); 213 thread.pushSlice(newSliceNamed('b', 1, 5)); 214 215 var ctr1 = model.getOrCreateProcess(1).getOrCreateCounter('cat', 'ctr1'); 216 ctr1.seriesNames.push('bytesallocated', 'bytesfree'); 217 ctr1.seriesColors.push(0, 1); 218 ctr1.timestamps.push(0, 10, 20); 219 ctr1.samples.push(0, 25, 10, 15, 20, 5); 220 assertEquals('ctr1', ctr1.name); 221 assertEquals(3, ctr1.numSamples); 222 assertEquals(2, ctr1.numSeries); 223 224 var ctr1track = new TimelineCounterTrack(); 225 ctr1track.counter = ctr1; 226 227 var threadTrack = new TimelineSliceTrack(); 228 threadTrack.slices = thread.slices; 229 230 var selection = new TimelineSelection(); 231 selection.addCounterSample(ctr1track, ctr1, 1); 232 threadTrack.addAllObjectsMatchingFilterToSelection( 233 new TimelineTitleFilter('b'), selection); 234 assertEquals(2, selection.length); 235 return selection; 236 } 237 238 function createSelectionWithNormallyDistributedSamples(numSlices) { 239 // Distance between start times is normally distributed, with mu = 16ms 240 // and sigma = 3ms. 241 var startTimes = [ 242 0, 18.4362262859, 32.5378088645, 44.8978868054, 243 63.4772725504, 77.438888345, 92.0102867913, 99.6208686689, 244 119.150576393, 137.54545468, 153.991587743, 175.456095568, 245 193.395772651, 205.691644582, 218.740054982, 239.308480724, 246 250.880949151, 268.528689601, 281.950478133, 296.791635722, 247 315.862427391, 333.954888221, 342.392899581, 362.364373939, 248 377.593380892, 392.296896748, 415.779941407, 435.517713864, 249 454.581222491, 470.329018858, 488.37029095, 502.283017166, 250 521.15141113, 534.36224697, 554.425018316, 574.89913248, 251 589.60294439, 604.780562233, 615.481610668, 630.055628965, 252 645.908449096, 661.776084055, 673.276049017, 689.776401428, 253 704.440135004, 716.33262401, 732.380086528, 743.970715322, 254 756.506690025, 772.391485532, 794.636984401, 803.801415494, 255 819.006502926, 837.610127549, 854.551103283, 875.170613672, 256 891.508235124, 905.263299017, 929.309555683, 943.417968804, 257 957.289319239, 972.302910569, 986.669355637, 1002.71558868, 258 1013.83359637, 1030.16840733, 1040.39503139, 1057.61583325, 259 1075.64709686, 1086.67671319, 1100.4617455, 1118.4871842, 260 1129.98143488, 1144.52318588, 1160.36966285, 1179.50049042, 261 1195.03088169, 1215.98199401, 1226.66591838, 1245.83650314, 262 1268.18058265, 1285.11047342, 1301.71570575, 1316.40723345, 263 1329.94342488, 1343.7569577, 1358.28267513, 1371.17560308, 264 1386.42247119, 1401.51767749, 1417.52489051, 1440.98712348, 265 1457.80113781, 1475.66079406, 1494.64137536, 1509.52941903, 266 1524.54762552, 1545.42960714, 1565.19444597, 1580.56308936, 267 1596.72211651]; 268 269 var events = []; 270 271 var model = new TimelineModel(); 272 var thread = model.getOrCreateProcess(52).getOrCreateThread(53); 273 var duration = 1; // 1ms 274 275 for (var i = 0; i < startTimes.length; ++i) { 276 for (var j = 0; j < numSlices; ++j) { 277 var name = 'slice' + String(numSlices - 1 - j); 278 thread.slices.push(newSliceNamed(name, startTimes[i], duration)); 279 } 280 } 281 282 var t53track = new TimelineThreadTrack(); 283 t53track.thread = model.processes[52].threads[53]; 284 285 var selection = new TimelineSelection(); 286 t53track.addAllObjectsMatchingFilterToSelection( 287 new TimelineTitleFilter('slice'), selection); 288 assertEquals(101 * numSlices, selection.length); 289 290 return selection; 291 } 292 293 function testAnalysisViewWithSingleSlice() { 294 var selection = createSelectionWithSingleSlice(); 295 296 var analysisEl = new TimelineAnalysisView(); 297 analysisEl.selection = selection; 298 this.addHTMLOutput(undefined, analysisEl); 299 } 300 301 function testAnalysisViewWithSingleSliceCategory() { 302 var selection = createSelectionWithSingleSlice(true); 303 304 var analysisEl = new TimelineAnalysisView(); 305 analysisEl.selection = selection; 306 this.addHTMLOutput(undefined, analysisEl); 307 } 308 309 function testAnalysisViewWithMultipleSlices() { 310 var selection = createSelectionWithTwoSlices(); 311 312 var analysisEl = new TimelineAnalysisView(); 313 analysisEl.selection = selection; 314 this.addHTMLOutput(undefined, analysisEl); 315 } 316 317 function testAnalysisViewWithMultipleSlicesSameTitle() { 318 var selection = createSelectionWithTwoSlicesSameTitle(); 319 320 var analysisEl = new TimelineAnalysisView(); 321 analysisEl.selection = selection; 322 this.addHTMLOutput(undefined, analysisEl); 323 } 324 325 function testAnalysisViewWithSingleCounterWithTwoSeries() { 326 var selection = createSelectionWithTwoSeriesSingleCounter(); 327 328 var analysisEl = new TimelineAnalysisView(); 329 analysisEl.selection = selection; 330 this.addHTMLOutput(undefined, analysisEl); 331 } 332 333 function testAnalysisViewWithTwoCountersWithTwoSeries() { 334 var selection = createSelectionWithTwoSeriesTwoCounters(); 335 336 var analysisEl = new TimelineAnalysisView(); 337 analysisEl.selection = selection; 338 this.addHTMLOutput(undefined, analysisEl); 339 } 340 341 function testAnalysisViewWithSingleSliceSingleCounter() { 342 var selection = createSelectionWithSingleSliceSingleCounter(); 343 344 var analysisEl = new TimelineAnalysisView(); 345 analysisEl.selection = selection; 346 this.addHTMLOutput(undefined, analysisEl); 347 } 348 349 function testSelectionWithNormallyDistributedSamples() { 350 var numSlices = 1; 351 var selection = createSelectionWithNormallyDistributedSamples(numSlices); 352 353 var analysisEl = new TimelineAnalysisView(); 354 analysisEl.selection = selection; 355 this.addHTMLOutput(undefined, analysisEl); 356 } 357 358 function StubAnalysisResults() { 359 this.tables = []; 360 } 361 StubAnalysisResults.prototype = { 362 __proto__: Object.protoype, 363 364 appendTable: function(parent, className) { 365 var table = { 366 className: className, 367 rows: [] 368 }; 369 table.className = className; 370 this.tables.push(table); 371 return table; 372 }, 373 374 appendTableHeader: function(table, label) { 375 if (table.tableHeader) 376 throw new Error('Only one summary header allowed.'); 377 table.tableHeader = label; 378 }, 379 380 appendSummaryRow: function(table, label, opt_text) { 381 table.rows.push({label: label, 382 text: opt_text}); 383 }, 384 385 appendSpacingRow: function(table) { 386 table.rows.push({spacing: true}); 387 }, 388 389 appendSummaryRowTime: function(table, label, time) { 390 table.rows.push({label: label, 391 time: time}); 392 }, 393 394 appendDataRow: function(table, label, duration, occurences, details) { 395 table.rows.push({label: label, 396 duration: duration, 397 occurences: occurences, 398 details: details}); 399 } 400 }; 401 402 function testAnalyzeSelectionWithSingleSlice() { 403 var selection = createSelectionWithSingleSlice(); 404 405 var results = new StubAnalysisResults(); 406 tracing.analyzeSelection_(results, selection); 407 assertEquals(1, results.tables.length); 408 var table = results.tables[0]; 409 assertEquals('Selected slice:', table.tableHeader); 410 assertEquals(3, table.rows.length); 411 412 assertEquals('b', table.rows[0].text); 413 assertEquals(0, table.rows[1].time); 414 assertAlmostEquals(0.002, table.rows[2].time); 415 } 416 417 function testAnalyzeSelectionWithFalsyArgs() { 418 var model = new TimelineModel(); 419 var t53 = model.getOrCreateProcess(52).getOrCreateThread(53); 420 var slice = newSliceNamed('b', 0, 0.002); 421 slice.args.bar = 0; 422 slice.args.foo = false; 423 t53.pushSlice(slice); 424 var t53track = new TimelineThreadTrack(); 425 t53track.thread = t53; 426 var selection = new TimelineSelection(); 427 t53track.addAllObjectsMatchingFilterToSelection( 428 new TimelineTitleFilter('b'), selection); 429 assertEquals(1, selection.length); 430 431 var analysisEl = new TimelineAnalysisView(); 432 analysisEl.selection = selection; 433 this.addHTMLOutput(undefined, analysisEl); 434 var rows = analysisEl.querySelectorAll('tr'); 435 assertEquals(rows.length, 7); 436 assertEquals(' bar', rows[5].children[0].textContent); 437 assertEquals('0', rows[5].children[1].textContent); 438 assertEquals(' foo', rows[6].children[0].textContent); 439 assertEquals('false', rows[6].children[1].textContent); 440 } 441 442 function testAnalyzeSelectionWithSingleSliceCategory() { 443 var selection = createSelectionWithSingleSlice(true); 444 445 var results = new StubAnalysisResults(); 446 tracing.analyzeSelection_(results, selection); 447 assertEquals(1, results.tables.length); 448 var table = results.tables[0]; 449 assertEquals('Selected slice:', table.tableHeader); 450 assertEquals(4, table.rows.length); 451 452 assertEquals('b', table.rows[0].text); 453 assertEquals('foo', table.rows[1].text); 454 assertEquals(0, table.rows[2].time); 455 assertAlmostEquals(0.002, table.rows[3].time); 456 } 457 458 function testAnalyzeSelectionWithTwoSlices() { 459 var selection = createSelectionWithTwoSlices(); 460 461 var results = new StubAnalysisResults(); 462 tracing.analyzeSelection_(results, selection); 463 assertEquals(1, results.tables.length); 464 var table = results.tables[0]; 465 assertEquals('Slices:', table.tableHeader); 466 assertEquals(6, table.rows.length); 467 468 assertEquals('a', table.rows[0].label); 469 assertEquals(1, table.rows[0].occurences); 470 assertAlmostEquals(0.04, table.rows[0].duration); 471 assertEquals('aa', table.rows[1].label); 472 assertEquals(1, table.rows[1].occurences); 473 assertAlmostEquals(0.06, table.rows[1].duration); 474 assertEquals('*Totals', table.rows[2].label); 475 assertEquals(2, table.rows[2].occurences); 476 assertAlmostEquals(0.1, table.rows[2].duration); 477 478 assertEquals('Selection start', table.rows[4].label); 479 assertAlmostEquals(0, table.rows[4].time); 480 481 assertEquals('Selection extent', table.rows[5].label); 482 assertAlmostEquals(0.18, table.rows[5].time); 483 } 484 485 function testAnalyzeSelectionWithTwoSlicesSameTitle() { 486 var selection = createSelectionWithTwoSlicesSameTitle(); 487 488 var results = new StubAnalysisResults(); 489 tracing.analyzeSelection_(results, selection); 490 assertEquals(1, results.tables.length); 491 var table = results.tables[0]; 492 493 assertEquals('Slices:', table.tableHeader); 494 assertEquals(5, table.rows.length); 495 496 assertEquals('c', table.rows[0].label); 497 assertEquals('2', table.rows[0].occurences); 498 assertAlmostEquals(0.04, table.rows[0].details.min); 499 assertAlmostEquals(0.05, table.rows[0].details.avg); 500 assertAlmostEquals(0.06, table.rows[0].details.max); 501 assertAlmostEquals(0.1, table.rows[0].duration); 502 assertEquals('*Totals', table.rows[1].label); 503 assertAlmostEquals(0.1, table.rows[1].duration); 504 assertEquals('Selection start', table.rows[3].label); 505 assertAlmostEquals(0, table.rows[3].time); 506 assertEquals('Selection extent', table.rows[4].label); 507 assertAlmostEquals(0.18, table.rows[4].time); 508 } 509 510 function testAnalyzeSelectionWithSingleCounter() { 511 var selection = createSelectionWithCounters(1); 512 513 var results = new StubAnalysisResults(); 514 tracing.analyzeSelection_(results, selection); 515 assertEquals(1, results.tables.length); 516 var table = results.tables[0]; 517 assertEquals('Selected counter:', table.tableHeader); 518 assertEquals(3, table.rows.length); 519 520 assertEquals('Title', table.rows[0].label); 521 assertEquals('Timestamp', table.rows[1].label); 522 assertEquals('value', table.rows[2].label); 523 assertEquals(10, table.rows[2].text); 524 } 525 526 function testAnalyzeSelectionWithBasicTwoSeriesTwoCounters() { 527 var selection = createSelectionWithTwoSeriesTwoCounters(); 528 529 var results = new StubAnalysisResults(); 530 tracing.analyzeSelection_(results, selection); 531 assertEquals(1, results.tables.length); 532 var table = results.tables[0]; 533 assertEquals('Counters:', table.tableHeader); 534 assertEquals(4, table.rows.length); 535 536 assertEquals('ctr1: bytesallocated', table.rows[0].label); 537 assertEquals('ctr1: bytesfree', table.rows[1].label); 538 assertEquals('ctr2: bytesallocated', table.rows[2].label); 539 assertEquals('ctr2: bytesfree', table.rows[3].label); 540 } 541 542 function testAnalyzeSelectionWithComplexSeriesTwoCounters() { 543 var selection = createSelectionWithTwoCountersDiffSeriesDiffHits(); 544 545 var results = new StubAnalysisResults(); 546 tracing.analyzeSelection_(results, selection); 547 assertEquals(1, results.tables.length); 548 var table = results.tables[0]; 549 assertEquals('Counters:', table.tableHeader); 550 assertEquals(3, table.rows.length); 551 552 assertEquals('a: bytesallocated', table.rows[0].label); 553 assertEquals('b: bytesallocated', table.rows[1].label); 554 assertEquals('b: bytesfree', table.rows[2].label); 555 } 556 557 function testAnalyzeSelectionWithCounterAndSlices() { 558 var selection = createSelectionWithSingleSliceSingleCounter(); 559 560 var results = new StubAnalysisResults(); 561 tracing.analyzeSelection_(results, selection); 562 assertEquals(2, results.tables.length); 563 var sliceTable = results.tables[0]; 564 var counterTable = results.tables[1]; 565 566 assertEquals('Selected slice:', sliceTable.tableHeader); 567 assertEquals(3, sliceTable.rows.length); 568 569 assertEquals('Selected counter:', counterTable.tableHeader); 570 assertEquals(4, counterTable.rows.length); 571 } 572 573 function testAnalyzeSelectionWithNormallyDistributedSamples() { 574 var numSlices = 2; 575 var selection = createSelectionWithNormallyDistributedSamples(numSlices); 576 577 var results = new StubAnalysisResults(); 578 tracing.analyzeSelection_(results, selection); 579 assertEquals(1, results.tables.length); 580 581 assertEquals('slice0', results.tables[0].rows[0].label); 582 assertEquals( 583 63, Math.round(results.tables[0].rows[0].details.frequency)); 584 assertEquals( 585 16, Math.round(results.tables[0].rows[0].details.frequency_stddev)); 586 587 assertEquals('slice1', results.tables[0].rows[1].label); 588 assertEquals( 589 63, Math.round(results.tables[0].rows[1].details.frequency)); 590 assertEquals( 591 16, Math.round(results.tables[0].rows[1].details.frequency_stddev)); 592 } 593 </script> 594 </body> 595 </html> 596