开玩笑 - 解决绩效问题
#javascript #性能 #jest

背景

Infogrid的这里,我们广泛使用开玩笑来进行单位测试。单位测试发生的典型情况之一是增加数量,很少有单位测试的体积减少。这意味着,当您开始使用这些测试遇到性能问题时,您知道问题只会变得更糟!

这正是我们所在的地方。实际上,性能下降是如此糟糕,以至于:

  • 开发人员很少在本地运行测试
  • 确实试图在当地进行所有测试的开发人员通常会冻结其机器
  • CI的速度并不多,但是鉴于CI过度依赖,您可能会等待测试反馈。

这篇文章总结了我在InfoGrid上进行的调查以及解决这些调查的措施。

首先,让我们拿一些指标。这是一张图表,显示我们的总执行时间在我们的CI跑步者的6个月内增加了一倍以上。

Time taken to run Jest Unit Tests

由于开发人员将其依赖于他们的测试运行,这实质上创造了一个非常缓慢的反馈循环,我们对此并不满意,因此我确实启动并修复了它。

值得注意的是,当我们启动这个发现过程时,我们是在开玩笑的V27上。

调查性能

调查和修复我们发现的缓慢下降的多个部分。当我们发现并深入研究时,我会尝试将其分解。本质上,它们分为三个主要部分,但我不会在这里透露它们,所以不要破坏文章。

测试执行时间与总过程Time

我研究的第一件事是,在进行测试的时间与过程本身所需的实际时间之间似乎存在很大的差异。例如,例如,如果我们查看一个文件,我们可以看到:

  • 总时间运行测试:112ms
  • 总过程时间:16.8S

â纱测试:nocoverage -watchall = false src/组件/材料 - ui/searchfilterinput/searchfilterinput.test.test.tsx
纱线运行v1.22.10
$ craco test -watchall = false src/组件/材料 - ui/searchfilterinput/searchfilterinput.test.tsx
通过SRC/组件/材料-UI/searchFilterInput/searchFilterInput.test.tsx(14.213 s)
searchFilterInput
正确渲染(112 ms)

测试套件:1通过,总共1个
测试:1通过,总共1个
快照:0总计
时间:14.788 S
运行所有测试套件匹配/src/components/material-ui/searchfilterinput/searchfilterinput.test.tsx/i。
在16.80s。

给出大约16.6秒的时间处理时间实际上并没有实际运行测试。实际上,实际运行测试的时间小于总时间的1%。实际上,有些文件花费了90多个运行...这是次优的。

这一点显然是测试本身并不慢。相反,关于玩笑的事情很慢。我花了一些时间使用--inspect-brk标志来分析玩笑,我考虑了如果是原因,我考虑了Typescript编译。最后,我在下一节中偶然发现了问题,与导入有关。

进口数量减慢开玩笑的dist

在一些轻巧的夜晚阅读中,我遇到了这个github问题https://github.com/jestjs/jest/issues/11234,其中详细介绍了“桶”档案可能会减慢开玩笑的速度。这真的很有趣,因为我们在某些文件中有大量进口,这也可能以与桶导入相同的方式影响开玩笑。

什么是枪管文件?11

在编写JavaScript的口味时,通常具有index.js文件的目录结构。

Example of a barrel directory structure with an index file exporting the public parts of the directory

枪管文件通常会重新出口。因此,可能具有以下内容:

export * from "./TimeBlock";
export * from "./constants"; // Not actually present in example image above

这样做的原因是,它使进口变得更加干净。它允许您替换:

import { TimeBlock } from "./TimeBlock/TimeBlock";
import { constants } from "./TimeBlock/constants";

with:

import { TimeBlock, constants } from "./TimeBlock";

测试桶进口是否影响US

要测试有关导入的理论,我创建了一个名为Foo.test.tsx的新文件,并使用了命令:

yarn test:nocoverage --clearCache && yarn test:nocoverage --watchAll=false src/foo.test.tsx

这可以确保在开始进行公平测试之前清除开玩笑的缓存,并仅对该文件进行测试。

基线

从对任何其他文件没有依赖性的测试开始:

describe('Foo', () => {
    it('should render correctly', () => {
        expect(true).toBe(true);
    });
});

通过src/foo.test.tsx
foo
应正确渲染(3 ms)

测试套件:1通过,总共1个
测试:1通过,总共1个
快照:0总计
时间:1.276 S
运行所有测试套件匹配/src/foo.test.tsx/i。
在8.03s完成。

测试本身的时间仅为3ms。整个过程时间花费了8s,虽然并不惊人,但至少为我们提供了测试的基线。

反应组件测试

鉴于我们的基准,现在让我们添加一个反应组件并进行更典型的测试,显然现在有一些依赖性。首先,我们将直接使用@testing-library/react

import '@testing-library/jest-dom/extend-expect';
import { render, screen } from '@testing-library/react';

describe('Foo', () => {
    it('should render correctly', () => {
        render(<p>Time period</p>);

        expect(screen.getByText('Time period')).toBeInTheDocument();
    });
});

> PASS  src/foo.test.tsx
> Foo
> ✓ should render correctly (3 ms)
> 
> Test Suites: 1 passed, 1 total
> Tests:       1 passed, 1 total
> Snapshots:   0 total
> Time:        1.416 s
> Ran all test suites matching /src/foo.test.tsx/i.
> Done in 8.70s.

因此,测试本身的时间仍然只有3ms。我们的ve边缘增加到8.7,因此包括额外的测试库。

典型的Infroid测试

因此,我们使用了包括的各种测试帮助者,因此我们通常不会像这样导入测试库。因此,通过对进口的稍微修改,我们可以重新测试正常的InfoGrid测试的外观。

import '@testing-library/jest-dom/extend-expect';
import { render, screen } from 'testUtils';

describe('Foo', () => {
    it('should render correctly', () => {
        render(<p>Time period</p>);

        expect(screen.getByText('Time period')).toBeInTheDocument();
    });
});

通过src/foo.test.tsx(69.894 s)
foo
应正确渲染(52 ms)

测试套件:1通过,总共1个
测试:1通过,总共1个
快照:0总计
时间:70.733 S
运行所有测试套件匹配/src/foo.test.tsx/i。
在81.55s。

完成

因此,测试本身添加了52毫秒,但在宏伟的方案中,我们不必为此担心。但是,我们通过仅更改单个导入语句来在测试时间中添加了高达72.85的时间。

挖掘testutils.js

因此,如果我们查看testutils.js文件,跳出的第一件事是导入的20行。其中一些进口来自大图书馆

import { MuiThemeProvider } from '@material-ui/core/styles';
import { render as rtlRender } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import isObject from 'lodash/isObject';
import PropTypes from 'prop-types';
import { HelmetProvider } from 'react-helmet-async';
import { QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { createStore } from 'redux';
import 'configuration/routes';

import { queryClient } from 'apiHooks/queryClient';
import rootReducer from 'configuration/reducers';
import { theme } from 'containers/AppContainer/theme';

import 'react-dates/initialize';

不幸的是,我们的代码/组件也会激怒这个问题。大多数文件都有大量导入,范围很容易从15-35个单独的导入范围。这里是一个特别糟糕的文件的一个例子:

import { differenceInDays } from 'date-fns';
import { Trans, useTranslation } from 'react-i18next';
import type { RouteConfigComponentProps } from 'react-router-config';

import { SUPPORT_EMAIL } from 'constants/contacts';
import { ORGANIZATION_FEATURE_FLAG } from 'utils/featureFlags';
import { useOrganizationFeature } from 'utils/hooks/user';
import { SENSOR_TYPE } from 'utils/types/ts/sensorConstants';
import { SOLUTIONS } from 'utils/types/ts/solutions';
import type { ViewSelectors } from 'views/solutions/components/DataContainer/DataContainer';
import DataSourceBlock from 'views/solutions/components/DataSourceBlock';
import { InsightsTabs } from 'views/solutions/components/Insights';
import SolutionPageError from 'views/solutions/components/SolutionPageError';
import TimePresetPicker from 'views/solutions/components/TimePresetPicker';
import {
    useTimeFilters,
    useLocationFilters,
    DATA_CONTAINER_BLOCK_VIEW,
} from 'views/solutions/hooks';

import PageLayout from '../../components/PageLayout';
import {
    userSolutionPageAvailability,
    userSolutionAvailability,
} from '../../decorators/permission';
import LiveData from './components/LiveData';
import AnalysisTab from './components/blocks/Analysis';
import DataTab from './components/blocks/Data';
import DayTimeBlock from './components/blocks/DayTimeBlock';
import MeetingRoomPersonCountBlock from './components/blocks/MeetingRoomPersonCountBlock';
import MostOccupiedBlock from './components/blocks/MostOccupiedBlock';
import OccupancyOverTime from './components/blocks/OccupancyOverTime';
import UsageOverTimeBlock from './components/blocks/UsageOverTimeBlock';
import { MOSV_TOOLTIP, OSV_TOOLTIP } from './constants';
import { useOccupancyPageStyles } from './styles';

修复#1

因此,我们的第一个修复程序是停止使用我们的testUtils.tsx文件。与其导入所有这些依赖关系,并为我们使用的每个第三方库设置多个React <ContextProvider>实例,而是仅在实际需要它们的测试中进行。

这确实添加了更多重复(我们创建了一些高阶组件以使其更容易),但是它可以节省大量时间,因此值得一提测试所需的最低要求。

内存泄漏S

我在分析时发现的下一件事是,我的测试时间大不相同。查看此输出:

@infogrid/solution-views-components:测试:通过src/solutioninsights/solutioninsights.test.tsx(331.823 s)
@infogrid/solution-views-components:测试:通过src/insights/card/locationsmodal/locationsmodal.test.tsx(459.442 s)
@infogrid/solution-views-components:测试:通过src/datacontainer/datacontainer.test.tsx(460.168 s)
@infogrid/solution-views-components:测试:通过src/header/header.test.tsx(283.344 s)
@infogrid/solution-views-components:测试:通过src/solutionSights/solutioninsightSitem/solutionSightSitem.test.test.tsx(461.045 s)
主:测试:通过src/views/dashboards/widgoards/healthybuildingscorewidget/content/comparemodal/compamodal.test.test.tsx(25.719 s)

在这里,我们有很多非常非常缓慢的测试。挑选DataContainer.test.tsx作为一个例子,我们看到只有460秒才能完成该1个测试文件。

但是,如果我们只运行单个测试,它就绘制了一个非常不同的故事:

takages/solution-views/compotents/compotents/components in Master [$]上的[$]通过€¢v16.14.0取7s
`时间pnpm测试src/datacontainer/datacontainer.test.tsx

@infogrid/solution-views-components@0.1.0 test/home/ian/src/webclient/packages/solution-views/组件
Jest -passwithnotests“ src/datacontainer/datacontainer.test.tsx”

通过src/datacontainer/datacontainer.test.tsx
DataContainer
应正确渲染(70 ms)
选择图表时,应以图表形式渲染数据(19 ms)
选择表(14 ms)

时,应在表格中渲染数据。

测试套件:1通过,总共1个
测试:3次通过,总共3个
快照:0总计
时间:4.966 s,估计461 s
运行所有测试套件匹配>/src \/datacontainer \/datacontainer.test.tsx/i。

真实的0m6.120s
用户0M6.336S
SYS 0M0.436S

我们可以在这里看到相同的测试文件需要少于5秒。这表明开玩笑本身并不是问题,这与系统有关。查看轶事证据是,当它开始用尽系统资源时,开玩笑会大大减慢。以上都是在12个核心上运行的(Intel®Core¢i7-10750h cpu @2.60GHzã12),带有20GB交换文件的32GB RAM机。

系统监视器的某些屏幕截图很有趣。运行所有测试始于CPU(所有核心最大)。
CPU Constrained

但是,一旦所有内存都被用完了,我们就会开始击中交换文件。在这一点上,我们将变为磁盘,并且CPU不再最大化。

Disk Constrained

交换文件接近容量后,系统开始变得不稳定,测试持续时间确实开始增加。应用程序开始冻结,机器无法使用,直到Jest完成为止。
Unstable System

检查存储器泄漏S

为什么会这样增加内存使用?我尝试针对节点运行内存跟踪。随着测试的运行(每个测试文件200-500MB之间),这表明潜在的内存泄漏时,它显示出堆的增加。下一步是确定这是我们的测试,还是开玩笑。

node -expose-gc ./ node_modules/@craco/craco/bin/bin/craco.js test-runinband -luninband - -logheapusage -watchall = false = false


通过SRC/组件/传感器/ConnectedCloudConnectors/connectedCloudConnectors.test.jsx(21.778 S,351 MB堆尺寸)
通过src/views/solutions/pages/smartcleaning/components/deskandspacecleaning/deskandspacecleaning.test.tsx(5.12 s,500 MB堆尺寸)
通过src/views/sensors/sensorslist/sensorslist.test.js(7.367 s,602 MB堆尺寸)
通过src/views/dashboards/widgoards/indoorairqualitywidget/indoorairqualitywidget.test.test.tsx(851 MB堆尺寸)
通过SRC/组件/材料-UI/sensorpicker/sensorpicker/sensorpicker.test.tsx(22.616 S,1394 MB堆尺寸)

github再次发出救援。我遇到了https://github.com/jestjs/jest/issues/11956,这表明节点16.11.0+和开玩笑的工作方式。果然,使用节点16.14.0(这是我们当时的内容),运行样品显示堆内存储器使用情况显着增加。

Memory Usage Comparison

修复#2

在这种情况下的修复,首先是在节点16.10.0固定所有问题,以解决直接疼痛点。然后,我们开始升级开玩笑到v29。完成此迁移后,我们切换到最新节点,并使用https://jestjs.io/docs/configuration/#workeridlememorylimit-numberstring应用了2GB限制。

Typescript Compilation

难题的最后一步是打字稿。我认为这不是一个开始时的问题,但是事实证明,玩笑实现了一个构建缓存层,这使我得出了一些不正确的结论。当您继续一遍又一遍地运行相同的测试以进行基准测试目的时,这会使您的数据弄乱,并且不会反映现实,在日常使用中,JEST可能需要经常重建代码。

因此,我开始运行测试,但要清除运行之间的开玩笑。这是一个图表,显示了使用ts-jest作为我们的跑步者的比较。如果缓存很冷,则需要20倍的时间来运行我们的MonorePo中一个包装的测试(大约100次测试)。刚刚运行的构建近10分钟,这很痛苦。

Cache vs Non Cache Jest Speed

修复#3

至此,我们已经引入了一些其他工具来优化我们的构建过程(主题为另一个帖子!),以减少我们正在运行的单元测试的数量。但是当他们确实需要跑步时,如果必须发生构建,那仍然太慢了。

因此,我们研究了ts-jest的替代工具。众所周知,打字稿编译器很慢,但是有一些努力,例如SWCESBuild试图解决这个问题。幸运的是,还有一些开玩笑的跑步者使用这些不同的构建工具!

Test runner comparison

上面的

是在有或没有开玩笑的情况下,在所有工具上运行的一组测试集。如您所见,当SWC和Esbuild击中缓存时,它们都大致相同(实际上比ts-jest稍慢),但是当他们错过缓存时,它们都会快得多。

因此,我们介绍的最后一个修复程序是为我们的每个包装切换到@swc/jest,从而大大减少了整体上的寒冷跑步时间。

值得注意的是,如果您走这条路线,开玩笑的行为有些不同,我们最终需要包括一个称为jest_workaround的软件包,并设置了我们的玩笑config,以便节省我们更新所有模拟

transform: {
        '^.+\\.[tj]sx?$': [
            '@swc/jest',
            {
                jsc: {
                    experimental: {
                        plugins: [['jest_workaround', {}]],
                    },
                },
            },
        ],
    },