<!DOCTYPE html>
<html lang="en">
<head>
<script> var __basePath = ''; </script>
<style> html, body { margin: 0; padding: 0; height: 100%; } </style>
    <script src="https://unpkg.com/ag-grid-enterprise@17.0.0/dist/ag-grid-enterprise.min.js"></script>    
    <link rel="stylesheet" href="styles.css">
</head>
<body>

<div style="height: 100%; padding-top: 120px; box-sizing: border-box;">
    <div id="myGrid" style="height: 100%;" class="ag-theme-balham-dark"></div>
</div>

<div style="position: absolute; top: 0px; left: 0px;">
    <div style="padding: 2px;">
        <span>Test:</span>

        <button onclick="onStartStress()">&#9658; Stress</button>
        <button onclick="onStartLoad()">&#9658; Load</button>
        <button onclick="onStopMessages()">&#9632; Stop</button>

        <span style="padding-left: 6px;">Columns:</span>

        <button onclick="onColumnsFlat()">Flat</button>
        <button onclick="onColumnsGroup()">Group</button>
        <button onclick="onColumnsPivot()">Pivot</button>

        <span style="padding-left: 6px;">Tool Panel:</span>

        <button onclick="onShowToolPanel()">Show</button>
        <button onclick="onHideToolPanel()">Hide</button>
    </div>

    <div style="padding: 2px; margin: 4px;">
        <span id="eMessage" class="message-box">
        </span>
    </div>

</div>

    <script src="main.js"></script>
</body>
</html>
var columnDefs = [
        // these are the row groups, so they are all hidden (they are showd in the group column)
        {headerName: 'Hierarchy', children: [
            {headerName: 'Product', field: 'product', type: 'dimension', rowGroupIndex: 0, hide: true},
            {headerName: 'Portfolio', field: 'portfolio', type: 'dimension', rowGroupIndex: 1, hide: true},
            {headerName: 'Book', field: 'book', type: 'dimension', rowGroupIndex: 2, hide: true}
        ]},

        // some string values, that do not get aggregated
        {headerName: 'Attributes', children: [
                {headerName: 'Trade', field: 'trade', width: 100},
                {headerName: 'Deal Type', field: 'dealType', type: 'dimension'},
                {headerName: 'Bid', field: 'bidFlag', type: 'dimension', width: 100}
        ]},

        // all the other columns (visible and not grouped)
        {headerName: 'Values', children: [
                {headerName: 'Current', field: 'current', type: 'measure'},
                {headerName: 'Previous', field: 'previous', type: 'measure'},
                {headerName: 'PL 1', field: 'pl1', type: 'measure'},
                {headerName: 'PL 2', field: 'pl2', type: 'measure'},
                {headerName: 'Gain-DX', field: 'gainDx', type: 'measure'},
                {headerName: 'SX / PX', field: 'sxPx', type: 'measure'},
                {headerName: '99 Out', field: '_99Out', type: 'measure'},
                {headerName: 'Submitter ID', field: 'submitterID', type: 'measure'},
                {headerName: 'Submitted Deal ID', field: 'submitterDealID', type: 'measure'}
        ]}
    ];

function numberCellFormatter(params) {
    return Math.floor(params.value).toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
}

var gridOptions = {
    columnTypes: {
        dimension: {
            enableRowGroup: true,
            enablePivot: true,
        },
        measure: {
            width: 150,
            aggFunc: 'sum',
            enableValue: true,
            cellClass: 'number',
            valueFormatter: numberCellFormatter,
            cellRenderer:'agAnimateShowChangeCellRenderer'
        }
    },
    columnDefs: columnDefs,
    enableStatusBar: true,
    animateRows: true,
    enableColResize: true,
    enableRangeSelection: true,
    enableSorting: true,
    rowGroupPanelShow: 'always',
    pivotPanelShow: 'always',
    suppressAggFuncInHeader: true,
    getRowNodeId: function(data) { return data.trade; },
    defaultColDef: {
        width: 120
    },
    onGridReady: function(params) {
        // gridOptions.api.sizeColumnsToFit();
    }
};

function onStartStress() {
    worker.postMessage('startStress');
}

function onStartLoad() {
    worker.postMessage('startLoad');
}

function onStopMessages() {
    worker.postMessage('stop');
    logMessage('Test stopped');
    console.log('Test stopped');
}

function onShowToolPanel() {
    gridOptions.api.showToolPanel(true);
}

function onHideToolPanel() {
    gridOptions.api.showToolPanel(false);
}

function onColumnsGroup() {
    gridOptions.columnApi.setPivotMode(false);
    gridOptions.columnApi.setColumnState([{"colId":"product","hide":true,"width":120,"rowGroupIndex":0},{"colId":"portfolio","hide":true,"width":120,"rowGroupIndex":1},{"colId":"book","hide":true,"width":120,"rowGroupIndex":2},{"colId":"trade","width":100},{"colId":"dealType","width":120},{"colId":"bidFlag","width":100},{"colId":"current","aggFunc":"sum","width":150},{"colId":"previous","aggFunc":"sum","width":150},{"colId":"pl1","aggFunc":"sum","width":150},{"colId":"pl2","aggFunc":"sum","width":150},{"colId":"gainDx","aggFunc":"sum","width":150},{"colId":"sxPx","aggFunc":"sum","width":150},{"colId":"_99Out","aggFunc":"sum","width":150},{"colId":"submitterID","aggFunc":"sum","width":150},{"colId":"submitterDealID","aggFunc":"sum","width":150}]);
}

function onColumnsPivot() {
    gridOptions.columnApi.setPivotMode(true);
    gridOptions.columnApi.setColumnState([{"colId":"product","hide":true,"width":120,"rowGroupIndex":0},{"colId":"portfolio","width":120,"pivotIndex":0},{"colId":"book","hide":true,"width":120,"rowGroupIndex":1},{"colId":"trade","width":100},{"colId":"dealType","width":120},{"colId":"bidFlag","width":100},{"colId":"current","aggFunc":"sum","width":150},{"colId":"previous","aggFunc":"sum","width":150},{"colId":"pl1","width":150},{"colId":"pl2","width":150},{"colId":"gainDx","width":150},{"colId":"sxPx","width":150},{"colId":"_99Out","width":150},{"colId":"submitterID","width":150},{"colId":"submitterDealID","width":150}]);
}

function onColumnsFlat() {
    gridOptions.columnApi.setPivotMode(false);
    gridOptions.columnApi.setColumnState([{"colId": "product", "width": 120}, {"colId": "portfolio", "width": 120}, {"colId": "book", "width": 120}, {"colId": "trade", "width": 100}, {"colId": "dealType", "width": 120}, {"colId": "bidFlag", "width": 100}, {"colId": "current", "width": 150}, {"colId": "previous", "width": 150}, {"colId": "pl1", "width": 150}, {"colId": "pl2", "width": 150}, {"colId": "gainDx", "width": 150}, {"colId": "sxPx", "width": 150}, {"colId": "_99Out", "width": 150}, {"colId": "submitterID", "width": 150}, {"colId": "submitterDealID", "width": 150}]);
}

var testStartTime;
var worker;

function startWorker() {

    worker = new Worker(__basePath + 'worker.js');

    worker.onmessage = function(e) {
        switch (e.data.type) {
            case 'start':
                testStartTime = new Date().getTime();
                logTestStart(e.data.messageCount, e.data.updateCount, e.data.interval);
                break;
            case 'end':
                logStressResults(e.data.messageCount, e.data.updateCount);
                break;
            case 'setRowData':
                gridOptions.api.setRowData(e.data.records);
                break;
            case 'updateData':
                gridOptions.api.batchUpdateRowData({update: e.data.records});
                break;
            default:
                console.log('unrecognised event type ' + e.type);
        }
    };
}

function logTestStart(messageCount, updateCount, interval) {
    let message = messageCount ?
        'Sending '+messageCount+' messages at once with '+updateCount+' record updates each.' :
        'Sending 1 message with '+updateCount+' updates every '+interval+' milliseconds, that\'s ' +(1000/interval*updateCount).toLocaleString()+ ' updates per second.';

    console.log(message);
    logMessage(message);
}

function logStressResults(messageCount, updateCount) {

    var testEndTime = new Date().getTime();
    var duration = testEndTime - testStartTime;
    var totalUpdates = messageCount * updateCount;

    var updatesPerSecond = Math.floor((totalUpdates / duration) * 1000);

    logMessage('Processed ' + totalUpdates.toLocaleString() + ' updates in ' + duration.toLocaleString() + 'ms, that\'s ' + updatesPerSecond.toLocaleString() + ' updates per second.')

    console.log('####################')
    console.log('# -- Stress test results --')
    console.log('# The grid was pumped with ' + messageCount.toLocaleString() + ' messages. Each message had ' + updateCount.toLocaleString() + ' record updates which gives a total number of updates of ' + totalUpdates.toLocaleString() + '.');
    console.log('# Time taken to execute the test was ' + duration.toLocaleString() + ' milliseconds which gives ' + updatesPerSecond.toLocaleString() + ' updates per second.');
    console.log('####################')
}

function logMessage(message) {
    document.querySelector('#eMessage').innerHTML = message;
}

// after page is loaded, create the grid.
document.addEventListener("DOMContentLoaded", function() {
    var eGridDiv = document.querySelector('#myGrid');
    new agGrid.Grid(eGridDiv, gridOptions);
    startWorker();
    onStartLoad();
});
body {
    font: 400 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
.number { text-align: right; }
.ag-row-level-0 { font-weight: bold; }
.ag-row-level-1 { color: lightblue; }
.ag-row-level-2 { color: lightyellow; }

.message-box {
    font-weight: bold;
}
// update these to change the stress test parameters
var STRESS_TEST_MESSAGE_COUNT = 1000;
var STRESS_TEST_UPDATES_PER_MESSAGE = 100;

// update these to change the
var LOAD_TEST_UPDATES_PER_MESSAGE = 100;
var LOAD_TEST_MILLISECONDS_BETWEEN_MESSAGES = 100;

// update these to change the size of the data initially loaded into the grid for updating
var BOOK_COUNT = 15;
var TRADE_COUNT = 5;

// add / remove products to change the data set
var PRODUCTS = ['Palm Oil','Rubber','Wool','Amber','Copper','Lead','Zinc','Tin','Aluminium',
    'Aluminium Alloy','Nickel','Cobalt','Molybdenum','Recycled Steel','Corn','Oats','Rough Rice',
    'Soybeans','Rapeseed','Soybean Meal','Soybean Oil','Wheat','Milk','Coca','Coffee C',
    'Cotton No.2','Sugar No.11','Sugar No.14'];

// add / remove portfolios to change the data set
var PORTFOLIOS = ['Aggressive','Defensive','Income','Speculative','Hybrid'];

// these are the list of columns that updates go to
var VALUE_FIELDS = ['current','previous','pl1','pl2','gainDx','sxPx','_99Out'];

// a list of the data, that we modify as we go. if you are using an immutable
// data store (such as Redux) then this would be similar to your store of data.
var globalRowData;

// start the book id's and trade id's at some future random number,
// looks more realistic than starting them at 0
var nextBookId = 62472;
var nextTradeId = 24287;
var nextBatchId = 101;

// build up the test data
function createRowData() {
    globalRowData = [];
    var thisBatch = nextBatchId++;
    for (var i = 0; i<PRODUCTS.length; i++) {
        var product = PRODUCTS[i];
        for (var j = 0; j<PORTFOLIOS.length; j++) {
            var portfolio = PORTFOLIOS[j];

            for (var k = 0; k<BOOK_COUNT; k++) {
                var book = createBookName();
                for (var l = 0; l < TRADE_COUNT; l++) {
                    var trade = createTradeRecord(product, portfolio, book, thisBatch);
                    globalRowData.push(trade);
                }
            }
        }
    }
    console.log('Total number of records sent to grid = ' + globalRowData.length);
}


// https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
function randomBetween(min,max) {
    return Math.floor(Math.random()*(max - min + 1)) + min;
}

function createTradeRecord(product, portfolio, book, batch) {
    var current = Math.floor(Math.random()*100000) + 100;
    var previous = current + Math.floor(Math.random()*10000) - 2000;
    var trade = {
        product: product,
        portfolio: portfolio,
        book: book,
        trade: createTradeId(),
        submitterID: randomBetween(10,1000),
        submitterDealID: randomBetween(10,1000),
        dealType: (Math.random()<.2) ? 'Physical' : 'Financial',
        bidFlag: (Math.random()<.5) ? 'Buy' : 'Sell',
        current: current,
        previous: previous,
        pl1: randomBetween(100,1000),
        pl2: randomBetween(100,1000),
        gainDx: randomBetween(100,1000),
        sxPx: randomBetween(100,1000),
        _99Out: randomBetween(100,1000),
        batch: batch
    };
    return trade;
}

function createBookName() {
    nextBookId++;
    return 'GL-' + nextBookId
}

function createTradeId() {
    nextTradeId++;
    return nextTradeId
}

createRowData();

postMessage({
    type: 'setRowData',
    records: globalRowData
});

var latestTestNumber = 0;

function updateSomeItems(updateCount) {
    var itemsToUpdate = [];
    for (var k = 0; k<updateCount; k++) {
        if (globalRowData.length === 0) { continue; }
        var indexToUpdate = Math.floor(Math.random()*globalRowData.length);
        var itemToUpdate = globalRowData[indexToUpdate];

        // make a copy of the item, and make some changes, so we are behaving
        // similar to how the
        var field = VALUE_FIELDS[Math.floor(Math.random() * VALUE_FIELDS.length)];
        itemToUpdate[field] = Math.floor(Math.random()*100000);

        itemsToUpdate.push(itemToUpdate);
    }
    return itemsToUpdate;
}

function sendMessagesWithThrottle(thisTestNumber) {
    var messageCount = null;

    postMessage({
        type: 'start',
        messageCount: messageCount,
        updateCount: LOAD_TEST_UPDATES_PER_MESSAGE,
        interval: LOAD_TEST_MILLISECONDS_BETWEEN_MESSAGES
    });

    var intervalId;

    function intervalFunc() {
        postMessage({
            type: 'updateData',
            records: updateSomeItems(LOAD_TEST_UPDATES_PER_MESSAGE)
        });
        if (thisTestNumber!==latestTestNumber) {
            clearInterval(intervalId);
        }
    }

    intervalId = setInterval(intervalFunc, LOAD_TEST_MILLISECONDS_BETWEEN_MESSAGES);
}

function sendMessagesNoThrottle() {
    postMessage({
        type: 'start',
        messageCount: STRESS_TEST_MESSAGE_COUNT,
        updateCount: STRESS_TEST_UPDATES_PER_MESSAGE,
        interval: null
    });

    // pump in 1000 messages without waiting
    for (var i = 0; i<=STRESS_TEST_MESSAGE_COUNT; i++) {
        postMessage({
            type: 'updateData',
            records: updateSomeItems(STRESS_TEST_UPDATES_PER_MESSAGE)
        });
    }

    postMessage({
        type: 'end',
        messageCount: STRESS_TEST_MESSAGE_COUNT,
        updateCount: STRESS_TEST_UPDATES_PER_MESSAGE
    });
}

self.addEventListener('message', function(e) {
    // any previous tests will see that this test number
    // has increased and will then stop their tests
    latestTestNumber++;
    switch (e.data) {
        case 'startStress':
            console.log('starting stress test');
            sendMessagesNoThrottle();
            break;
        case 'startLoad':
            console.log('starting load test');
            sendMessagesWithThrottle(latestTestNumber);
            break;
        case 'stopTest':
            console.log('stopping test');
            // sendMessagesNoThrottle();
            break;
        default:
            console.log('unknown message type ' + e.data);
            break;
    }
});