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