github.com/hernad/nomad@v1.6.112/ui/tests/unit/utils/allocation-stats-tracker-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import EmberObject from '@ember/object'; 7 import { assign } from '@ember/polyfills'; 8 import { module, test } from 'qunit'; 9 import sinon from 'sinon'; 10 import Pretender from 'pretender'; 11 import AllocationStatsTracker, { 12 stats, 13 } from 'nomad-ui/utils/classes/allocation-stats-tracker'; 14 import fetch from 'nomad-ui/utils/fetch'; 15 import statsTrackerFrameMissingBehavior from './behaviors/stats-tracker-frame-missing'; 16 17 import { settled } from '@ember/test-helpers'; 18 19 module('Unit | Util | AllocationStatsTracker', function () { 20 const refDate = Date.now() * 1000000; 21 const makeDate = (ts) => new Date(ts / 1000000); 22 23 const MockAllocation = (overrides) => 24 assign( 25 { 26 id: 'some-identifier', 27 taskGroup: { 28 reservedCPU: 200, 29 reservedMemory: 512, 30 tasks: [ 31 { 32 name: 'log-shipper', 33 reservedCPU: 50, 34 reservedMemory: 128, 35 lifecycleName: 'poststop', 36 }, 37 { 38 name: 'service', 39 reservedCPU: 100, 40 reservedMemory: 256, 41 lifecycleName: 'main', 42 }, 43 { 44 name: 'sidecar', 45 reservedCPU: 50, 46 reservedMemory: 128, 47 lifecycleName: 'prestart-sidecar', 48 }, 49 ], 50 }, 51 }, 52 overrides 53 ); 54 55 const mockFrame = (step) => ({ 56 ResourceUsage: { 57 CpuStats: { 58 TotalTicks: step + 100, 59 }, 60 MemoryStats: { 61 RSS: (step + 400) * 1024 * 1024, 62 Usage: (step + 800) * 1024 * 1024, 63 }, 64 }, 65 Tasks: { 66 service: { 67 ResourceUsage: { 68 CpuStats: { 69 TotalTicks: step + 50, 70 }, 71 MemoryStats: { 72 RSS: (step + 100) * 1024 * 1024, 73 Usage: (step + 200) * 1024 * 1024, 74 }, 75 }, 76 Timestamp: refDate + step, 77 }, 78 'log-shipper': { 79 ResourceUsage: { 80 CpuStats: { 81 TotalTicks: step + 25, 82 }, 83 MemoryStats: { 84 RSS: (step + 50) * 1024 * 1024, 85 Usage: (step + 100) * 1024 * 1024, 86 }, 87 }, 88 Timestamp: refDate + step * 10, 89 }, 90 sidecar: { 91 ResourceUsage: { 92 CpuStats: { 93 TotalTicks: step + 26, 94 }, 95 MemoryStats: { 96 RSS: (step + 51) * 1024 * 1024, 97 Usage: (step + 101) * 1024 * 1024, 98 }, 99 }, 100 Timestamp: refDate + step * 100, 101 }, 102 }, 103 Timestamp: refDate + step * 1000, 104 }); 105 106 test('the AllocationStatsTracker constructor expects a fetch definition and an allocation', async function (assert) { 107 const tracker = AllocationStatsTracker.create(); 108 assert.throws( 109 () => { 110 tracker.fetch(); 111 }, 112 /StatsTrackers need a fetch method/, 113 'Polling does not work without a fetch method provided' 114 ); 115 }); 116 117 test('the url property is computed based off the allocation id', async function (assert) { 118 const allocation = MockAllocation(); 119 const tracker = AllocationStatsTracker.create({ fetch, allocation }); 120 121 assert.equal( 122 tracker.get('url'), 123 `/v1/client/allocation/${allocation.id}/stats`, 124 'Url is derived from the allocation id' 125 ); 126 }); 127 128 test('reservedCPU and reservedMemory properties come from the allocation', async function (assert) { 129 const allocation = MockAllocation(); 130 const tracker = AllocationStatsTracker.create({ fetch, allocation }); 131 132 assert.equal( 133 tracker.get('reservedCPU'), 134 allocation.taskGroup.reservedCPU, 135 'reservedCPU comes from the allocation task group' 136 ); 137 assert.equal( 138 tracker.get('reservedMemory'), 139 allocation.taskGroup.reservedMemory, 140 'reservedMemory comes from the allocation task group' 141 ); 142 }); 143 144 test('the tasks list comes from the allocation', async function (assert) { 145 assert.expect(7); 146 147 const allocation = MockAllocation(); 148 const tracker = AllocationStatsTracker.create({ fetch, allocation }); 149 150 assert.equal( 151 tracker.get('tasks.length'), 152 allocation.taskGroup.tasks.length, 153 'tasks matches lengths with the allocation task group' 154 ); 155 allocation.taskGroup.tasks.forEach((task) => { 156 const trackerTask = tracker.get('tasks').findBy('task', task.name); 157 assert.equal( 158 trackerTask.reservedCPU, 159 task.reservedCPU, 160 `CPU matches for task ${task.name}` 161 ); 162 assert.equal( 163 trackerTask.reservedMemory, 164 task.reservedMemory, 165 `Memory matches for task ${task.name}` 166 ); 167 }); 168 }); 169 170 test('poll results in requesting the url and calling append with the resulting JSON', async function (assert) { 171 const allocation = MockAllocation(); 172 const tracker = AllocationStatsTracker.create({ 173 fetch, 174 allocation, 175 append: sinon.spy(), 176 }); 177 const mockFrame = { 178 Some: { 179 data: ['goes', 'here'], 180 twelve: 12, 181 }, 182 }; 183 184 const server = new Pretender(function () { 185 this.get('/v1/client/allocation/:id/stats', () => [ 186 200, 187 {}, 188 JSON.stringify(mockFrame), 189 ]); 190 }); 191 192 tracker.get('poll').perform(); 193 194 assert.equal(server.handledRequests.length, 1, 'Only one request was made'); 195 assert.equal( 196 server.handledRequests[0].url, 197 `/v1/client/allocation/${allocation.id}/stats`, 198 'The correct URL was requested' 199 ); 200 201 await settled(); 202 assert.ok( 203 tracker.append.calledWith(mockFrame), 204 'The JSON response was passed onto append as a POJO' 205 ); 206 207 server.shutdown(); 208 }); 209 210 test('append appropriately maps a data frame to the tracked stats for cpu and memory for the allocation as well as individual tasks', async function (assert) { 211 const allocation = MockAllocation(); 212 const tracker = AllocationStatsTracker.create({ fetch, allocation }); 213 214 assert.deepEqual(tracker.get('cpu'), [], 'No tracked cpu yet'); 215 assert.deepEqual(tracker.get('memory'), [], 'No tracked memory yet'); 216 217 assert.deepEqual( 218 tracker.get('tasks'), 219 [ 220 { 221 task: 'service', 222 reservedCPU: 100, 223 reservedMemory: 256, 224 cpu: [], 225 memory: [], 226 }, 227 { 228 task: 'sidecar', 229 reservedCPU: 50, 230 reservedMemory: 128, 231 cpu: [], 232 memory: [], 233 }, 234 { 235 task: 'log-shipper', 236 reservedCPU: 50, 237 reservedMemory: 128, 238 cpu: [], 239 memory: [], 240 }, 241 ], 242 'tasks represents the tasks for the allocation with no stats yet' 243 ); 244 245 tracker.append(mockFrame(1)); 246 247 assert.deepEqual( 248 tracker.get('cpu'), 249 [{ timestamp: makeDate(refDate + 1000), used: 101, percent: 101 / 200 }], 250 'One frame of cpu' 251 ); 252 assert.deepEqual( 253 tracker.get('memory'), 254 [ 255 { 256 timestamp: makeDate(refDate + 1000), 257 used: 401 * 1024 * 1024, 258 percent: 401 / 512, 259 }, 260 ], 261 'One frame of memory' 262 ); 263 assert.deepEqual( 264 tracker.get('tasks'), 265 [ 266 { 267 task: 'service', 268 reservedCPU: 100, 269 reservedMemory: 256, 270 cpu: [ 271 { 272 timestamp: makeDate(refDate + 1), 273 used: 51, 274 percent: 51 / 100, 275 percentStack: 51 / (100 + 50 + 50), 276 percentTotal: 51 / (100 + 50 + 50), 277 }, 278 ], 279 memory: [ 280 { 281 timestamp: makeDate(refDate + 1), 282 used: 101 * 1024 * 1024, 283 percent: 101 / 256, 284 percentStack: 101 / (256 + 128 + 128), 285 percentTotal: 101 / (256 + 128 + 128), 286 }, 287 ], 288 }, 289 { 290 task: 'sidecar', 291 reservedCPU: 50, 292 reservedMemory: 128, 293 cpu: [ 294 { 295 timestamp: makeDate(refDate + 100), 296 used: 27, 297 percent: 27 / 50, 298 percentStack: (27 + 51) / (100 + 50 + 50), 299 percentTotal: 27 / (100 + 50 + 50), 300 }, 301 ], 302 memory: [ 303 { 304 timestamp: makeDate(refDate + 100), 305 used: 52 * 1024 * 1024, 306 percent: 52 / 128, 307 percentStack: (52 + 101) / (256 + 128 + 128), 308 percentTotal: 52 / (256 + 128 + 128), 309 }, 310 ], 311 }, 312 { 313 task: 'log-shipper', 314 reservedCPU: 50, 315 reservedMemory: 128, 316 cpu: [ 317 { 318 timestamp: makeDate(refDate + 10), 319 used: 26, 320 percent: 26 / 50, 321 percentStack: (26 + 27 + 51) / (100 + 50 + 50), 322 percentTotal: 26 / (100 + 50 + 50), 323 }, 324 ], 325 memory: [ 326 { 327 timestamp: makeDate(refDate + 10), 328 used: 51 * 1024 * 1024, 329 percent: 51 / 128, 330 percentStack: (51 + 52 + 101) / (256 + 128 + 128), 331 percentTotal: 51 / (256 + 128 + 128), 332 }, 333 ], 334 }, 335 ], 336 'tasks represents the tasks for the allocation, each with one frame of stats' 337 ); 338 339 tracker.append(mockFrame(2)); 340 341 assert.deepEqual( 342 tracker.get('cpu'), 343 [ 344 { timestamp: makeDate(refDate + 1000), used: 101, percent: 101 / 200 }, 345 { timestamp: makeDate(refDate + 2000), used: 102, percent: 102 / 200 }, 346 ], 347 'Two frames of cpu' 348 ); 349 assert.deepEqual( 350 tracker.get('memory'), 351 [ 352 { 353 timestamp: makeDate(refDate + 1000), 354 used: 401 * 1024 * 1024, 355 percent: 401 / 512, 356 }, 357 { 358 timestamp: makeDate(refDate + 2000), 359 used: 402 * 1024 * 1024, 360 percent: 402 / 512, 361 }, 362 ], 363 'Two frames of memory' 364 ); 365 366 assert.deepEqual( 367 tracker.get('tasks'), 368 [ 369 { 370 task: 'service', 371 reservedCPU: 100, 372 reservedMemory: 256, 373 cpu: [ 374 { 375 timestamp: makeDate(refDate + 1), 376 used: 51, 377 percent: 51 / 100, 378 percentStack: 51 / (100 + 50 + 50), 379 percentTotal: 51 / (100 + 50 + 50), 380 }, 381 { 382 timestamp: makeDate(refDate + 2), 383 used: 52, 384 percent: 52 / 100, 385 percentStack: 52 / (100 + 50 + 50), 386 percentTotal: 52 / (100 + 50 + 50), 387 }, 388 ], 389 memory: [ 390 { 391 timestamp: makeDate(refDate + 1), 392 used: 101 * 1024 * 1024, 393 percent: 101 / 256, 394 percentStack: 101 / (256 + 128 + 128), 395 percentTotal: 101 / (256 + 128 + 128), 396 }, 397 { 398 timestamp: makeDate(refDate + 2), 399 used: 102 * 1024 * 1024, 400 percent: 102 / 256, 401 percentStack: 102 / (256 + 128 + 128), 402 percentTotal: 102 / (256 + 128 + 128), 403 }, 404 ], 405 }, 406 { 407 task: 'sidecar', 408 reservedCPU: 50, 409 reservedMemory: 128, 410 cpu: [ 411 { 412 timestamp: makeDate(refDate + 100), 413 used: 27, 414 percent: 27 / 50, 415 percentStack: (27 + 51) / (100 + 50 + 50), 416 percentTotal: 27 / (100 + 50 + 50), 417 }, 418 { 419 timestamp: makeDate(refDate + 200), 420 used: 28, 421 percent: 28 / 50, 422 percentStack: (28 + 52) / (100 + 50 + 50), 423 percentTotal: 28 / (100 + 50 + 50), 424 }, 425 ], 426 memory: [ 427 { 428 timestamp: makeDate(refDate + 100), 429 used: 52 * 1024 * 1024, 430 percent: 52 / 128, 431 percentStack: (52 + 101) / (256 + 128 + 128), 432 percentTotal: 52 / (256 + 128 + 128), 433 }, 434 { 435 timestamp: makeDate(refDate + 200), 436 used: 53 * 1024 * 1024, 437 percent: 53 / 128, 438 percentStack: (53 + 102) / (256 + 128 + 128), 439 percentTotal: 53 / (256 + 128 + 128), 440 }, 441 ], 442 }, 443 { 444 task: 'log-shipper', 445 reservedCPU: 50, 446 reservedMemory: 128, 447 cpu: [ 448 { 449 timestamp: makeDate(refDate + 10), 450 used: 26, 451 percent: 26 / 50, 452 percentStack: (26 + 27 + 51) / (100 + 50 + 50), 453 percentTotal: 26 / (100 + 50 + 50), 454 }, 455 { 456 timestamp: makeDate(refDate + 20), 457 used: 27, 458 percent: 27 / 50, 459 percentStack: (27 + 28 + 52) / (100 + 50 + 50), 460 percentTotal: 27 / (100 + 50 + 50), 461 }, 462 ], 463 memory: [ 464 { 465 timestamp: makeDate(refDate + 10), 466 used: 51 * 1024 * 1024, 467 percent: 51 / 128, 468 percentStack: (51 + 52 + 101) / (256 + 128 + 128), 469 percentTotal: 51 / (256 + 128 + 128), 470 }, 471 { 472 timestamp: makeDate(refDate + 20), 473 used: 52 * 1024 * 1024, 474 percent: 52 / 128, 475 percentStack: (52 + 53 + 102) / (256 + 128 + 128), 476 percentTotal: 52 / (256 + 128 + 128), 477 }, 478 ], 479 }, 480 ], 481 'tasks represents the tasks for the allocation, each with two frames of stats' 482 ); 483 }); 484 485 test('each stat list has maxLength equal to bufferSize', async function (assert) { 486 assert.expect(16); 487 488 const allocation = MockAllocation(); 489 const bufferSize = 10; 490 const tracker = AllocationStatsTracker.create({ 491 fetch, 492 allocation, 493 bufferSize, 494 }); 495 496 for (let i = 1; i <= 20; i++) { 497 tracker.append(mockFrame(i)); 498 } 499 500 assert.equal( 501 tracker.get('cpu.length'), 502 bufferSize, 503 `20 calls to append, only ${bufferSize} frames in the stats array` 504 ); 505 assert.equal( 506 tracker.get('memory.length'), 507 bufferSize, 508 `20 calls to append, only ${bufferSize} frames in the stats array` 509 ); 510 511 assert.equal( 512 +tracker.get('cpu')[0].timestamp, 513 +makeDate(refDate + 11000), 514 'Old frames are removed in favor of newer ones' 515 ); 516 assert.equal( 517 +tracker.get('memory')[0].timestamp, 518 +makeDate(refDate + 11000), 519 'Old frames are removed in favor of newer ones' 520 ); 521 522 tracker.get('tasks').forEach((task) => { 523 assert.equal( 524 task.cpu.length, 525 bufferSize, 526 `20 calls to append, only ${bufferSize} frames in the stats array` 527 ); 528 assert.equal( 529 task.memory.length, 530 bufferSize, 531 `20 calls to append, only ${bufferSize} frames in the stats array` 532 ); 533 }); 534 535 assert.equal( 536 +tracker.get('tasks').findBy('task', 'service').cpu[0].timestamp, 537 +makeDate(refDate + 11), 538 'Old frames are removed in favor of newer ones' 539 ); 540 assert.equal( 541 +tracker.get('tasks').findBy('task', 'service').memory[0].timestamp, 542 +makeDate(refDate + 11), 543 'Old frames are removed in favor of newer ones' 544 ); 545 546 assert.equal( 547 +tracker.get('tasks').findBy('task', 'log-shipper').cpu[0].timestamp, 548 +makeDate(refDate + 110), 549 'Old frames are removed in favor of newer ones' 550 ); 551 assert.equal( 552 +tracker.get('tasks').findBy('task', 'log-shipper').memory[0].timestamp, 553 +makeDate(refDate + 110), 554 'Old frames are removed in favor of newer ones' 555 ); 556 557 assert.equal( 558 +tracker.get('tasks').findBy('task', 'sidecar').cpu[0].timestamp, 559 +makeDate(refDate + 1100), 560 'Old frames are removed in favor of newer ones' 561 ); 562 assert.equal( 563 +tracker.get('tasks').findBy('task', 'sidecar').memory[0].timestamp, 564 +makeDate(refDate + 1100), 565 'Old frames are removed in favor of newer ones' 566 ); 567 }); 568 569 test('the stats computed property macro constructs an AllocationStatsTracker based on an allocationProp and a fetch definition', async function (assert) { 570 const allocation = MockAllocation(); 571 const fetchSpy = sinon.spy(); 572 573 const SomeClass = EmberObject.extend({ 574 stats: stats('alloc', function () { 575 return () => fetchSpy(this); 576 }), 577 }); 578 const someObject = SomeClass.create({ 579 alloc: allocation, 580 }); 581 582 assert.equal( 583 someObject.get('stats.url'), 584 `/v1/client/allocation/${allocation.id}/stats`, 585 'stats computed property macro creates an AllocationStatsTracker' 586 ); 587 588 someObject.get('stats').fetch(); 589 590 assert.ok( 591 fetchSpy.calledWith(someObject), 592 'the fetch factory passed into the macro gets called to assign a bound version of fetch to the AllocationStatsTracker instance' 593 ); 594 }); 595 596 test('changing the value of the allocationProp constructs a new AllocationStatsTracker', async function (assert) { 597 const alloc1 = MockAllocation(); 598 const alloc2 = MockAllocation(); 599 const SomeClass = EmberObject.extend({ 600 stats: stats('alloc', () => fetch), 601 }); 602 603 const someObject = SomeClass.create({ 604 alloc: alloc1, 605 }); 606 607 const stats1 = someObject.get('stats'); 608 609 someObject.set('alloc', alloc2); 610 const stats2 = someObject.get('stats'); 611 612 assert.notStrictEqual( 613 stats1, 614 stats2, 615 'Changing the value of alloc results in creating a new AllocationStatsTracker instance' 616 ); 617 }); 618 619 statsTrackerFrameMissingBehavior({ 620 resourceName: 'allocation', 621 ResourceConstructor: MockAllocation, 622 TrackerConstructor: AllocationStatsTracker, 623 mockFrame, 624 compileResources(frame) { 625 const timestamp = makeDate(frame.Timestamp); 626 const cpu = frame.ResourceUsage.CpuStats.TotalTicks; 627 const memory = frame.ResourceUsage.MemoryStats.RSS; 628 return [ 629 { 630 timestamp, 631 used: cpu, 632 percent: cpu / 200, 633 }, 634 { 635 timestamp, 636 used: memory, 637 percent: memory / 1024 / 1024 / 512, 638 }, 639 ]; 640 }, 641 }); 642 });