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  });