Проблема с производительностью сетки kendo (kendo grid + angular js + web api)

Проблема :

У меня есть Kendo Grid на моей HTML-странице Angular JS. Данные Kendo Grid поступают из моего удаленного веб-API службы.

Сетка Kendo пытается загрузить в браузер контент размером 38 МБ для каждых 10 записей, пока мы выполняем пейджинг (или загрузку страницы в первый раз), и это занимает ок. 6 минут на загрузку данных.

Что и какой контент он скачивает за 38 МБ?

То, что я уже реализовал, прочитав / изучив аналогичные билеты поддержки при переполнении стека:

  1. Реализован серверный пейджинг true (pageSize = 10, Всего записей = примерно 56000)

  2. Объединение JS и CSS

  3. Я попробовал оба варианта ниже:

    прокручиваемый: {virtual: true} ИЛИ ЖЕ scrollable: {endless: true}

  4. Я проверяю свою хранимую процедуру на производстве, она выполняется менее чем за 3 секунды в течение ок. 55000 записей. (на производстве и постановке обоих серверов).

  5. Я проверяю свой контроллер веб-API, он возвращает ответ менее чем за 4 секунды сетке кендо, а затем сетка кендо занимает слишком много времени для заполнения данных.

  6. У меня ниже JS и CSS для Kendo (уже реализованная комплектация):

    • Kendo.all.min.js
    • kendo.bootstrap.min.css
    • kendo.common-bootstrap.min.css
    • Другие необходимые для проекта JS и CSS также загружаются в виде пакета.

Ниже приведены страницы моих живых проектов:

HTML-страница:

<div id="heatMapGrid" kendo-grid k-options="vm.heatMapGridOptions"></div>

Контроллер AngularJS:

var dataSourceHeatMapGrid = new kendo.data.DataSource({
            transport: {
                read: function (options) {

                    heatMapService.getHeatMapGrid(options.data, heatMapGridParams)
                        .then(function (response) {
                            options.success(response.data);
                            $rootScope.optioncallback = options;

                            //$scope.htmapGridCSV = [];
                            //$scope.htmapGridCSV = response.data.exportData;

                        }).catch(function (e) {
                            console.log('Error: ', e);
                            throw e;
                        });
                },
                parameterMap: function (options) {
                    return JSON.stringify(options);
                }
            },
            schema: {
                data: function (response) {
                    return response.gridData;
                },
                total: function (response) {
                    return response.Total;
                },
                model: {
                    fields: {
                        TPID: { type: "number" },
                        TPName: { type: "string" },
                        EndCustomerPurchaseAmt: { type: "number" },
                        PrimaryExpirationMonth: { type: "string" },
                        AgreementID: { type: "number" },
                        TotalPurchased: { type: "number" },
                        TotalAssigned: { type: "number" },
                        OverUnder: { type: "number" },
                        VSEntPurchasedUnits: { type: "number" },
                        VSProMSDNUnits: { type: "number" },
                        VSTestProMSDNUnits: { type: "number" },
                        MSDNPlat: { type: "number" },
                        CloudPurchasedUnits: { type: "number" },
                        UnbilledOverage: { type: "number" },
                        AzurePotentialRevenue: { type: "number" }
                    }
                }
            },
            pageSize: 10,
            serverPaging: true
        });

vm.heatMapGridOptions = {
            columns: [
                { "title": "", template: "<a title='#=TPID#' #=isPinnedAccount==1 ? \"class='terrunpinaccount'\" : \"class='terrpinaccount'\"# ng-click='vm.pinUnpinAccount(\"#=TPID#\")'></a>" },
                { "title": "Account Name", "field": "TPName", template: "<a href='javascript:void(0);' ng-click='vm.tPIDDetails(\"#=TPID#\",\"#=TPName#\")' title='#=TPName#'><div class='DisplayTitleTPName'>#=TPName#<ul><li>AM: #=AM#, OM: #=OperatingModel#, Country: #=Country#</li><li>DevSales Lead: #=SalesLead#, SSP: #=Dev_SSP#, TSP: #=DevTSP#</li></ul></div></a>", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                {
                    "title": "PERFORMANCE AND ANNIVERSARIES", headerAttributes: { style: "text-align: center;font-weight: bold;" },
                    columns:
                    [{
                        "title": "Renewals and True Ups", headerAttributes: { style: "text-align: center;font-weight: bold;" },
                        columns: [{ "title": "Total Annualized Expiring", "field": "EndCustomerPurchaseAmt", format: "{0:c0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                                { "title": "Primary Anniversary Month", "field": "PrimaryExpirationMonth", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                                { "title": "Agreement Number", "field": "AgreementID", format: "{0:n0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } }]
                    }]
                },
                {
                    "title": "EFFECTIVE LICENSE POSITIONS", headerAttributes: { style: "text-align: center;font-weight: bold;" },
                    columns:
                    [{
                        "title": "Visual Studio Subscriptions", headerAttributes: { style: "text-align: center;font-weight: bold;" },
                        columns: [{ "title": "Purchased", "field": "TotalPurchased", format: "{0:n0}" },
                                { "title": "Assigned", "field": "TotalAssigned", format: "{0:n0}" },
                                { "title": "Over Under", "field": "OverUnder", format: "{0:n0}" }]
                    },
                    {
                        "title": "Account Footprint (Active SA Licenses)", headerAttributes: { style: "text-align: center;font-weight: bold;" },
                        columns: [{ "title": "Enterprise w/ MSDN", "field": "VSEntPurchasedUnits", format: "{0:n0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                                { "title": "Pro w/ MSDN", "field": "VSProMSDNUnits", format: "{0:n0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                                { "title": "Test Pro w/ MSDN", "field": "VSTestProMSDNUnits", format: "{0:n0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                                { "title": "MSDN Platforms", "field": "MSDNPlat", format: "{0:n0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                                { "title": "Cloud", "field": "CloudPurchasedUnits", format: "{0:n0}" }]
                    },
                    {
                        "title": "Azure", headerAttributes: { style: "text-align: center;font-weight: bold;" },
                        columns: [{ "title": "Unbilled Overage", "field": "UnbilledOverage", format: "{0:c0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } },
                                { "title": "Potential Revenue", "field": "AzurePotentialRevenue", headerTemplate: '<span title="Potential Revue is based on the delta of activated seats and <br/> developers deploying to Azure multiplied by the annual <br/> value of an Azure attached developer ($15k)">Potential Revenue</span>', format: "{0:c0}", headerAttributes: { style: "white-space: normal; overflow: visible;" } }]
                    }]
                }
            ],
            groupable: false,
            sortable: true,
            resizable: true,
            //pageable: true,
            pageable: {
                refresh: true,
                pageSizes: [10, 20, 50],

            },
            columnMenu: true,
            scrollable: false
            //filterable: true
        };

$("#heatMapGrid").data("kendoGrid").setDataSource(dataSourceHeatMapGrid);

Сервис AngularJS:

services.getHeatMapGrid = function (command, heatMapGridParams) {
            var data = {
                page: command.page,
                pageSize: command.pageSize,
                skip: command.skip,
                take: command.take,
                alias: heatMapGridParams.alias,
                hasDevTest: heatMapGridParams.hasDevTest,
                hasDevTestLabs: heatMapGridParams.hasDevTestLabs,
                hasXamarin: heatMapGridParams.hasXamarin,
                devOpsMSShopFlag: heatMapGridParams.devOpsMSShopFlag,
                devOpsOSSThirdPartyShopsFlag: heatMapGridParams.devOpsOSSThirdPartyShopsFlag,
                intelligentAppsFlag: heatMapGridParams.intelligentAppsFlag,
                paaSServicesFlag: heatMapGridParams.paaSServicesFlag,
                enterpriseStepUpFlag: heatMapGridParams.enterpriseStepUpFlag,
                devsLearningAzureFlag: heatMapGridParams.devsLearningAzureFlag,
                devOpsAcceleratorEligibleFlag: heatMapGridParams.devOpsAcceleratorEligibleFlag,
                overAssignedFlag: heatMapGridParams.overAssignedFlag,
                lowAssignmentsFlag: heatMapGridParams.lowAssignmentsFlag,
                hasCloudSubscriptionFlag: heatMapGridParams.hasCloudSubscriptionFlag,
                hasUnbilledOverageFlag: heatMapGridParams.hasUnbilledOverageFlag,
                hasDevTestOppty: heatMapGridParams.hasDevTestOppty,
                areaID: heatMapGridParams.areaID,
                countryID: heatMapGridParams.countryID,
                segmentID: heatMapGridParams.segmentID,
                subsegmentID: heatMapGridParams.subsegmentID,
                salesUnitID: heatMapGridParams.salesUnitID,
                agreementRenewalOrTrueupID: heatMapGridParams.agreementRenewalOrTrueupID,
                aM: heatMapGridParams.aM,
                industry: heatMapGridParams.industry,
                hasAppServOppty: heatMapGridParams.hasAppServOppty,
                hasDotNetDeveloperFlag: heatMapGridParams.hasDotNetDeveloperFlag,
                paaSReadyFlag: heatMapGridParams.paaSReadyFlag,
                startMonth: heatMapGridParams.startMonth,
                endMonth: heatMapGridParams.endMonth
            };
            return $http({ method: 'GET', url: config.apiUrl + 'Account/HeatMapGrid/', params: data });
        };

Веб-API:

[HttpGet]
    public heatMapGridAndExport HeatMapGrid([FromUri]HeatMapGridModel model)
    {
        ListView listView = new ListView();

        List<getHeatMapDataGlobalFilter_Result> listGridDataForTotalCount = new List<getHeatMapDataGlobalFilter_Result>();
        listGridDataForTotalCount = listView.GetListViewGridData(model.alias, model.hasDevTest, model.hasDevTestLabs, model.hasXamarin, model.devOpsMSShopFlag, model.devOpsOSSThirdPartyShopsFlag, model.intelligentAppsFlag, model.paaSServicesFlag, model.enterpriseStepUpFlag, model.devsLearningAzureFlag, model.devOpsAcceleratorEligibleFlag, model.overAssignedFlag, model.lowAssignmentsFlag, model.hasCloudSubscriptionFlag, model.hasUnbilledOverageFlag, model.hasDevTestOppty, model.areaID, model.countryID, model.segmentID, model.subsegmentID, model.salesUnitID, model.agreementRenewalOrTrueupID, model.aM, model.industry, model.hasAppServOppty, model.hasDotNetDeveloperFlag, model.paaSReadyFlag, model.startMonth, model.endMonth);

        List<getHeatMapDataGlobalFilter_Result> listGridData = new List<getHeatMapDataGlobalFilter_Result>();
        listGridData = listGridDataForTotalCount.Skip(model.skip).Take(model.take).OrderByDescending(c => c.EndCustomerPurchaseAmt).ToList();

                    //List<heatMapExport> listExportData = new List<heatMapExport>();
        //listExportData = listGridDataForTotalCount.Select(c => new heatMapExport()
        //{
        //    TPName = c.TPName,
        //    TPID = c.TPID,
        //    OperatingModel = c.OperatingModel,
        //    Area = c.Area,
        //    Country = c.Country,
        //    CreditedRegion = c.CreditedRegion,
        //    CreditedDistrict = c.CreditedDistrict,
        //    Segment = c.Segment,
        //    ATUManager = c.ATUManager,
        //    Dev_SSP = c.Dev_SSP,
        //    AM = c.AM,
        //    Industry = c.Industry,
        //    ATSName = c.ATSName,
        //    AssignedPect = string.Format("{0:p0}", c.AssignedPect),
        //    ActivatedPect = string.Format("{0:p0}", c.ActivatedPect),
        //    AzureActivated = Convert.ToString(c.AzureActivated),
        //    EndCustomerPurchaseAmt = string.Format("{0:c0}", c.EndCustomerPurchaseAmt),
        //    PrimaryExpirationMonth = Convert.ToString(c.PrimaryExpirationMonth),
        //    AgreementID = Convert.ToString(c.AgreementID),
        //    TotalPurchased = string.Format("{0:n0}", c.TotalPurchased),
        //    TotalAssigned = string.Format("{0:n0}", c.TotalAssigned),
        //    OverUnder = string.Format("{0:n0}", c.OverUnder),
        //    VSEntPurchasedUnits = string.Format("{0:n0}", c.VSEntPurchasedUnits),
        //    VSProMSDNUnits = string.Format("{0:n0}", c.VSProMSDNUnits),
        //    VSTestProMSDNUnits = string.Format("{0:n0}", c.VSTestProMSDNUnits),
        //    MSDNPlat = string.Format("{0:n0}", c.MSDNPlat),
        //    CloudPurchasedUnits = string.Format("{0:n0}", c.CloudPurchasedUnits),
        //    UnbilledOverage = string.Format("{0:c0}", c.UnbilledOverage),
        //    AzurePotentialRevenue = string.Format("{0:c0}", c.AzurePotentialRevenue)
        //}).ToList();
        var heatMapData = new heatMapGridAndExport
        {
            gridData = listGridData,
            //exportData = listExportData,
            Total = listGridDataForTotalCount.Count()
        };
        return heatMapData;
    }

Моя среда:

  1. Версия Telerik Control - Kendo UI v2017.2.621

  2. Машина для разработки операционной системы - Windows 10 Enterprise (8 ГБ ОЗУ, процессор Intel Core i7, 64-разрядная версия) (клиентская ОС)

  3. Браузер - Google Chrome, версия 65.0.3325.181

  4. .NET Framework - версия 4.6.1

  5. Visual Studio - Enterprise 2015, версия 14.0.25431.01 (обновление 3)

  6. Язык программирования - C#

Вот снимок экрана моего браузера для производственного сервера:

Here is my production server kendo grid, which takes almost 12 minutes to populate data in kendo grid, and you can see TTFB (Time To First Byte. This timing includes 1 round trip of latency and the time the server took to prepare the response) for this request is 3.26 seconds

Вот еще один снимок экрана, когда я нажимаю на вторую страницу, он снова загружает содержимое размером 38 МБ, и это занимает ок. 6 мин. (подкачка сервера = true и pageSize = 10)

enter image description here

Скриншоты отладки кода:

enter image description here

Что я делаю не так? Может кто-нибудь мне помочь.

Заранее спасибо.

Можете ли вы отключить строку, в которой вы присоединяете ~ 56K строк данных экспорта к ответу WebAPI, и посмотреть, как он работает? (ответ ниже, для ясности). Я подозреваю, что это твоя проблема

Joe Glover 11.04.2018 18:20

@JoeGlover, Спасибо за ответ. но я уже пробовал, комментируя всю эту функцию экспорта, но это не влияет на время или контент, который он загружает. Т.е. ничего не улучшается, если убрать функцию экспорта. Любая другая помощь приветствуется. Еще раз спасибо.

Kashyap Vyas 12.04.2018 07:37

Это помогло бы точно увидеть, что находится в этих 38 МБ, либо указав разрыв вашего WebAPI непосредственно перед тем, как он вернет ответ (из вашего предыдущего комментария, я предполагаю, что heatMapData.gridData должен содержать весь набор данных), либо проверив JSON, поскольку он появляется на проводе. Вы должны иметь возможность углубиться в это в инструментах разработчика Chrome или, в качестве альтернативы, использовать внешний инструмент, такой как Fiddler, Wireshark и т. Д.

Joe Glover 12.04.2018 11:16

Еще одна вещь, которую стоит попробовать, - это упорядочить вызовы ваших методов Linq. Я изменяю свой первоначальный ответ, чтобы показать, что я имею в виду.

Joe Glover 12.04.2018 11:18

@JoeGlover ранее я тоже думал о том же, но затем я проверил heatMapData.gridData в ответе WebAPI, отладив код, а также проверил в скрипте, но в обоих случаях он содержит только 10 записей, а не весь набор данных (поскольку я использовал пропустить и взять) И для заказа я проверил с помощью пункта о порядке замены перед пропуском / принятием, но без эффекта, а также попытался удалить порядок, но в обоих случаях не повезло. Любая другая помощь приветствуется. Спасибо

Kashyap Vyas 13.04.2018 10:39

Хорошо, это разочаровывает. Если перед тем, как WebAPI вернет ответ, все выглядит нормально, значит, ответ должен находиться в трассировке скрипта. Может быть, в заголовки прикреплено что-то массивное? Это должно быть что-то подобное, если ваши данные в порядке. Для сравнения, у меня есть сетка, которая обычно запрашивает 11,4 КБ / 10 строк, 174 КБ / 100 строк, 3 МБ / 1000 строк. Скрипач по-прежнему показывает размер сообщений около 38 МБ?

Joe Glover 13.04.2018 12:41

@JoeGlover, я проверил скрипач, ДА, он показывает размер 38 МБ после ответа. И последний объект ответа содержит три вещи: 1. gridData = 10 записей 2. exportData = 55000 записей 3. Total = 55000 (Count = int number) Я приложил снимок экрана для отладки кода разработчика. не могли бы вы взглянуть

Kashyap Vyas 16.04.2018 14:12
1
7
883
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Можете ли вы отключить строку, в которой вы присоединяете ~ 56K строк данных экспорта к ответу WebAPI, и посмотреть, как он работает? Я подозреваю, что это твоя проблема

    var heatMapData = new heatMapGridAndExport
    {
        gridData = listGridData,
        //exportData = listExportData,  //perhaps make conditional, for export only?
        Total = listGridDataForTotalCount.Count()
    };
    return heatMapData;

Изменить: поскольку это, похоже, не устранило вашу проблему, можете ли вы попробовать переупорядочить вызовы метода Linq, как это, поскольку поведение, которое вы получаете, подразумевает, что возвращается весь набор результатов?

listGridData =
    listGridDataForTotalCount.OrderByDescending(c => c.EndCustomerPurchaseAmt)
        .Skip(model.skip).Take(model.take).ToList();

Я не знаю наверняка, но мне интересно, заставляет ли наличие последнего OrderByDescending Linq вернуться ко всему набору результатов, и это то, что вы в конечном итоге получаете от ToList?

Некоторый дополнительный контекст и описание улучшили бы этот ответ.

KevinO 11.04.2018 19:33

@JoeGlover, я попытался переупорядочить вызовы метода Linq, как вы говорите, но не повезло.

Kashyap Vyas 13.04.2018 10:43

@KevinO, Любая помощь будет принята с благодарностью.

Kashyap Vyas 13.04.2018 10:44

@JoeGlover, неожиданно, что раньше я удалял экспорт, но не влиял на производительность (и контент), но теперь я снова попробовал то же самое и обнаружил, что данные экспорта вызывают проблему. Я удалил его и удалил кодировку gzip, чтобы сделать это еще быстрее. Спасибо

Kashyap Vyas 17.04.2018 15:23

Не то чтобы неожиданно, это случается со всеми нами время от времени! Спасибо, что сообщили мне, что проблема решена.

Joe Glover 18.04.2018 14:33

Другие вопросы по теме