vmui: add a comparison of data to the Cardinality Explorer (#4123)

* feat: add button "show today" to date picker

* feat: add comparison with the prev day (#3967)

* vmui/docs: add comparison of data to cardinality page
This commit is contained in:
Yury Molodov 2023-04-25 11:21:57 +02:00 committed by Aliaksandr Valialkin
parent 382a2ff649
commit 3c45256736
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
19 changed files with 227 additions and 59 deletions

View file

@ -1,4 +1,4 @@
@use "../../../../styles/variables" as *; @use "src/styles/variables" as *;
.vm-tenant-input { .vm-tenant-input {
position: relative; position: relative;

View file

@ -8,6 +8,7 @@ import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import classNames from "classnames"; import classNames from "classnames";
import MonthsList from "./MonthsList/MonthsList"; import MonthsList from "./MonthsList/MonthsList";
import Button from "../../Button/Button";
interface DatePickerProps { interface DatePickerProps {
date: Date | Dayjs date: Date | Dayjs
@ -29,6 +30,9 @@ const Calendar: FC<DatePickerProps> = ({
const [viewType, setViewType] = useState<CalendarTypeView>(CalendarTypeView.days); const [viewType, setViewType] = useState<CalendarTypeView>(CalendarTypeView.days);
const [viewDate, setViewDate] = useState(dayjs.tz(date)); const [viewDate, setViewDate] = useState(dayjs.tz(date));
const [selectDate, setSelectDate] = useState(dayjs.tz(date)); const [selectDate, setSelectDate] = useState(dayjs.tz(date));
const today = dayjs().startOf("day").tz();
const viewDateIsToday = today.format() === viewDate.format();
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const toggleDisplayYears = () => { const toggleDisplayYears = () => {
@ -44,6 +48,10 @@ const Calendar: FC<DatePickerProps> = ({
setSelectDate(date); setSelectDate(date);
}; };
const handleToday = () => {
setViewDate(today);
};
useEffect(() => { useEffect(() => {
if (selectDate.format() === dayjs.tz(date).format()) return; if (selectDate.format() === dayjs.tz(date).format()) return;
onChange(selectDate.format(format)); onChange(selectDate.format(format));
@ -88,6 +96,17 @@ const Calendar: FC<DatePickerProps> = ({
onChangeViewDate={handleChangeViewDate} onChangeViewDate={handleChangeViewDate}
/> />
)} )}
{!viewDateIsToday && (viewType === CalendarTypeView.days) && (
<div className="vm-calendar-footer">
<Button
variant="text"
size="small"
onClick={handleToday}
>
show today
</Button>
</div>
)}
</div> </div>
); );
}; };

View file

@ -11,6 +11,7 @@ interface CalendarBodyProps {
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSelectDate }) => { const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSelectDate }) => {
const format = "YYYY-MM-DD";
const today = dayjs().tz().startOf("day"); const today = dayjs().tz().startOf("day");
const days: (Dayjs|null)[] = useMemo(() => { const days: (Dayjs|null)[] = useMemo(() => {
@ -45,10 +46,10 @@ const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSel
"vm-calendar-body-cell": true, "vm-calendar-body-cell": true,
"vm-calendar-body-cell_day": true, "vm-calendar-body-cell_day": true,
"vm-calendar-body-cell_day_empty": !d, "vm-calendar-body-cell_day_empty": !d,
"vm-calendar-body-cell_day_active": (d && d.toISOString()) === selectDate.startOf("day").toISOString(), "vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
"vm-calendar-body-cell_day_today": (d && d.toISOString()) === today.toISOString() "vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format)
})} })}
key={d ? d.toISOString() : i} key={d ? d.format(format) : i}
onClick={createHandlerSelectDate(d)} onClick={createHandlerSelectDate(d)}
> >
{d && d.format("D")} {d && d.format("D")}

View file

@ -166,4 +166,10 @@
} }
} }
} }
&-footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
} }

View file

@ -10,12 +10,14 @@ import "./style.scss";
export interface CardinalityTotalsProps { export interface CardinalityTotalsProps {
totalSeries: number; totalSeries: number;
totalSeriesAll: number; totalSeriesAll: number;
totalSeriesPrev: number;
totalLabelValuePairs: number; totalLabelValuePairs: number;
seriesCountByMetricName: TopHeapEntry[]; seriesCountByMetricName: TopHeapEntry[];
} }
const CardinalityTotals: FC<CardinalityTotalsProps> = ({ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
totalSeries, totalSeries,
totalSeriesPrev,
totalSeriesAll, totalSeriesAll,
seriesCountByMetricName seriesCountByMetricName
}) => { }) => {
@ -27,11 +29,14 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
const isMetric = /__name__/.test(match || ""); const isMetric = /__name__/.test(match || "");
const progress = seriesCountByMetricName[0]?.value / totalSeriesAll * 100; const progress = seriesCountByMetricName[0]?.value / totalSeriesAll * 100;
const diff = totalSeries - totalSeriesPrev;
const dynamic = Math.abs(diff) / totalSeriesPrev * 100;
const totals = [ const totals = [
{ {
title: "Total series", title: "Total series",
value: totalSeries.toLocaleString("en-US"), value: totalSeries.toLocaleString("en-US"),
dynamic: !totalSeries || !totalSeriesPrev ? "" : `${dynamic.toFixed(2)}%`,
display: !focusLabel, display: !focusLabel,
info: `The total number of active time series. info: `The total number of active time series.
A time series is uniquely identified by its name plus a set of its labels. A time series is uniquely identified by its name plus a set of its labels.
@ -57,20 +62,33 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
"vm-cardinality-totals_mobile": isMobile "vm-cardinality-totals_mobile": isMobile
})} })}
> >
{totals.map(({ title, value, info }) => ( {totals.map(({ title, value, info, dynamic }) => (
<div <div
className="vm-cardinality-totals-card" className="vm-cardinality-totals-card"
key={title} key={title}
> >
<div className="vm-cardinality-totals-card-header"> <h4 className="vm-cardinality-totals-card__title">
{title}
{info && ( {info && (
<Tooltip title={<p className="vm-cardinality-totals-card-header__tooltip">{info}</p>}> <Tooltip title={<p className="vm-cardinality-totals-card__tooltip">{info}</p>}>
<div className="vm-cardinality-totals-card-header__info-icon"><InfoIcon/></div> <div className="vm-cardinality-totals-card__info-icon"><InfoIcon/></div>
</Tooltip> </Tooltip>
)} )}
<h4 className="vm-cardinality-totals-card-header__title">{title}</h4> </h4>
</div>
<span className="vm-cardinality-totals-card__value">{value}</span> <span className="vm-cardinality-totals-card__value">{value}</span>
{!!dynamic && (
<Tooltip title={`in relation to the previous day: ${totalSeriesPrev.toLocaleString("en-US")}`}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive vm-dynamic-number_down": diff < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": diff > 0,
})}
>
{dynamic}
</span>
</Tooltip>
)}
</div> </div>
))} ))}
</div> </div>

View file

@ -5,25 +5,20 @@
flex-wrap: wrap; flex-wrap: wrap;
align-content: flex-start; align-content: flex-start;
justify-content: flex-start; justify-content: flex-start;
gap: $padding-global; gap: $padding-medium;
flex-grow: 1; flex-grow: 1;
&_mobile { &_mobile {
gap: $padding-small; gap: $padding-global;
justify-content: center; justify-content: center;
} }
&-card { &-card {
display: flex; display: grid;
grid-template-columns: auto 1fr;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 4px; gap: $padding-small 4px;
&-header {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&__info-icon { &__info-icon {
width: 12px; width: 12px;
@ -34,27 +29,28 @@
} }
&__title { &__title {
font-weight: bold; display: flex;
align-items: center;
justify-content: flex-start;
gap: 4px;
grid-column: 1/-1;
color: $color-text; color: $color-text;
&:after {
content: ':';
}
} }
&__tooltip { &__tooltip {
max-width: 280px; max-width: 280px;
white-space: normal; white-space: normal;
padding: $padding-small; padding: $padding-small;
line-height: 130%; line-height: $font-size;
font-size: $font-size; font-size: $font-size;
} }
}
&__value { &__value {
font-weight: bold; font-weight: bold;
color: $color-primary; color: $color-primary;
font-size: $font-size-medium; font-size: $font-size-large;
line-height: $font-size;
text-align: center;
} }
} }
} }

View file

@ -18,6 +18,7 @@ interface MetricsProperties {
tabs: string[]; tabs: string[];
chartContainer: MutableRef<HTMLDivElement> | undefined; chartContainer: MutableRef<HTMLDivElement> | undefined;
totalSeries: number, totalSeries: number,
totalSeriesPrev: number,
sectionTitle: string; sectionTitle: string;
tip?: string; tip?: string;
tableHeaderCells: HeadCell[]; tableHeaderCells: HeadCell[];
@ -28,6 +29,7 @@ const MetricsContent: FC<MetricsProperties> = ({
tabs: tabsProps = [], tabs: tabsProps = [],
chartContainer, chartContainer,
totalSeries, totalSeries,
totalSeriesPrev,
onActionClick, onActionClick,
sectionTitle, sectionTitle,
tip, tip,
@ -40,6 +42,7 @@ const MetricsContent: FC<MetricsProperties> = ({
<TableCells <TableCells
row={row} row={row}
totalSeries={totalSeries} totalSeries={totalSeries}
totalSeriesPrev={totalSeriesPrev}
onActionClick={onActionClick} onActionClick={onActionClick}
/> />
); );

View file

@ -26,7 +26,7 @@ const EnhancedTable: FC<TableProps> = ({
const sortedData = stableSort(rows, getComparator(order, orderBy)); const sortedData = stableSort(rows, getComparator(order, orderBy));
return ( return (
<table className="vm-table"> <table className="vm-table vm-cardinality-panel-table">
<EnhancedTableHead <EnhancedTableHead
order={order} order={order}
orderBy={orderBy} orderBy={orderBy}

View file

@ -4,15 +4,27 @@ import LineProgress from "../../../../components/Main/LineProgress/LineProgress"
import { PlayCircleOutlineIcon } from "../../../../components/Main/Icons"; import { PlayCircleOutlineIcon } from "../../../../components/Main/Icons";
import Button from "../../../../components/Main/Button/Button"; import Button from "../../../../components/Main/Button/Button";
import Tooltip from "../../../../components/Main/Tooltip/Tooltip"; import Tooltip from "../../../../components/Main/Tooltip/Tooltip";
import classNames from "classnames";
interface CardinalityTableCells { interface CardinalityTableCells {
row: Data, row: Data,
totalSeries: number; totalSeries: number;
totalSeriesPrev: number;
onActionClick: (name: string) => void; onActionClick: (name: string) => void;
} }
const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick }) => { const TableCells: FC<CardinalityTableCells> = ({
row,
totalSeries,
totalSeriesPrev,
onActionClick
}) => {
const progress = totalSeries > 0 ? row.value / totalSeries * 100 : -1; const progress = totalSeries > 0 ? row.value / totalSeries * 100 : -1;
const progressPrev = totalSeriesPrev > 0 ? row.valuePrev / totalSeriesPrev * 100 : -1;
const hasProgresses = [progress, progressPrev].some(p => p === -1);
const diffPercent = progress - progressPrev;
const relationPrevDay = hasProgresses ? "" : `${diffPercent.toFixed(2)}%`;
const handleActionClick = () => { const handleActionClick = () => {
onActionClick(row.name); onActionClick(row.name);
@ -35,13 +47,42 @@ const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick
key={row.value} key={row.value}
> >
{row.value} {row.value}
{!!row.diff && (
<Tooltip title={`in relation to the previous day: ${row.valuePrev}`}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive": row.diff < 0,
"vm-dynamic-number_negative": row.diff > 0,
})}
>
&nbsp;{row.diff > 0 ? "+" : ""}{row.diff}
</span>
</Tooltip>
)}
</td> </td>
{progress > 0 && ( {progress > 0 && (
<td <td
className="vm-table-cell" className="vm-table-cell"
key={row.progressValue} key={row.progressValue}
> >
<div className="vm-cardinality-panel-table__progress">
<LineProgress value={progress}/> <LineProgress value={progress}/>
{relationPrevDay && (
<Tooltip title={"in relation to the previous day"}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive vm-dynamic-number_down": diffPercent < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": diffPercent > 0,
})}
>
{relationPrevDay}
</span>
</Tooltip>
)}
</div>
</td> </td>
)} )}
<td <td

View file

@ -12,7 +12,7 @@ export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
}; };
return ( return (
<thead className="vm-table-header"> <thead className="vm-table-header vm-cardinality-panel-table__header">
<tr className="vm-table__row vm-table__row_header"> <tr className="vm-table__row vm-table__row_header">
{headerCells.map((headCell) => ( {headerCells.map((headCell) => (
<th <th

View file

@ -28,6 +28,8 @@ export interface TableProps {
export interface Data { export interface Data {
name: string; name: string;
value: number; value: number;
diff: number;
valuePrev: number;
progressValue: number; progressValue: number;
actions: string; actions: string;
} }

View file

@ -28,8 +28,9 @@ export default class AppConfigurator {
get defaultTSDBStatus(): TSDBStatus { get defaultTSDBStatus(): TSDBStatus {
return { return {
totalSeries: 0, totalSeries: 0,
totalLabelValuePairs: 0, totalSeriesPrev: 0,
totalSeriesByAll: 0, totalSeriesByAll: 0,
totalLabelValuePairs: 0,
seriesCountByMetricName: [], seriesCountByMetricName: [],
seriesCountByLabelName: [], seriesCountByLabelName: [],
seriesCountByFocusLabelValue: [], seriesCountByFocusLabelValue: [],
@ -142,11 +143,11 @@ export default class AppConfigurator {
}; };
} }
totalSeries(keyName: string): number { totalSeries(keyName: string, prev = false): number {
if (keyName === "labelValueCountByLabelName") { if (keyName === "labelValueCountByLabelName") {
return -1; return -1;
} }
return this.tsdbStatus.totalSeries; return prev ? this.tsdbStatus.totalSeriesPrev : this.tsdbStatus.totalSeries;
} }
} }

View file

@ -33,24 +33,51 @@ export const useFetchQuery = (): {
setIsLoading(true); setIsLoading(true);
setTSDBStatus(appConfigurator.defaultTSDBStatus); setTSDBStatus(appConfigurator.defaultTSDBStatus);
const defaultParams = { date: requestParams.date, topN: 0, match: "", focusLabel: "" } as CardinalityRequestsParams; const totalParams = {
const url = getCardinalityInfo(serverUrl, requestParams); date: requestParams.date,
const urlDefault = getCardinalityInfo(serverUrl, defaultParams); topN: 0,
match: "",
focusLabel: ""
} as CardinalityRequestsParams;
const prevDayParams = {
...requestParams,
date: dayjs(requestParams.date).subtract(1, "day").tz().format(DATE_FORMAT),
} as CardinalityRequestsParams;
const urlBase = getCardinalityInfo(serverUrl, requestParams);
const urlPrev = getCardinalityInfo(serverUrl, prevDayParams);
const uslTotal = getCardinalityInfo(serverUrl, totalParams);
const urls = [urlBase, urlPrev, uslTotal];
try { try {
const response = await fetch(url); const responses = await Promise.all(urls.map(url => fetch(url)));
const resp = await response.json(); const [resp, respPrev, respTotals] = await Promise.all(responses.map(resp => resp.json()));
const responseTotal = await fetch(urlDefault); if (responses[0].ok) {
const respTotals = await responseTotal.json(); const { data: dataTotal } = respTotals;
if (response.ok) { const prevResult = { ...respPrev.data } as TSDBStatus;
const { data } = resp; const result = { ...resp.data } as TSDBStatus;
const { totalSeries } = respTotals.data; result.totalSeriesByAll = dataTotal?.totalSeries;
const result = { ...data } as TSDBStatus; result.totalSeriesPrev = prevResult?.totalSeries;
result.totalSeriesByAll = totalSeries;
const name = match?.replace(/[{}"]/g, ""); const name = match?.replace(/[{}"]/g, "");
result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name); result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name);
Object.keys(result).forEach(k => {
const key = k as keyof TSDBStatus;
const entries = result[key];
const prevEntries = prevResult[key];
if (Array.isArray(entries) && Array.isArray(prevEntries)) {
entries.forEach((entry) => {
const valuePrev = prevEntries.find(prevEntry => prevEntry.name === entry.name)?.value;
entry.diff = valuePrev ? entry.value - valuePrev : 0;
entry.valuePrev = valuePrev || 0;
});
}
});
setTSDBStatus(result); setTSDBStatus(result);
setIsLoading(false); setIsLoading(false);
} else { } else {

View file

@ -55,6 +55,7 @@ const CardinalityPanel: FC = () => {
{isLoading && <Spinner message={spinnerMessage}/>} {isLoading && <Spinner message={spinnerMessage}/>}
<CardinalityConfigurator <CardinalityConfigurator
totalSeries={tsdbStatusData.totalSeries} totalSeries={tsdbStatusData.totalSeries}
totalSeriesPrev={tsdbStatusData.totalSeriesPrev}
totalSeriesAll={tsdbStatusData.totalSeriesByAll} totalSeriesAll={tsdbStatusData.totalSeriesByAll}
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs} totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
seriesCountByMetricName={tsdbStatusData.seriesCountByMetricName} seriesCountByMetricName={tsdbStatusData.seriesCountByMetricName}
@ -80,6 +81,7 @@ const CardinalityPanel: FC = () => {
onActionClick={handleFilterClick(keyName)} onActionClick={handleFilterClick(keyName)}
tabs={defaultState.tabs[keyName as keyof Tabs]} tabs={defaultState.tabs[keyName as keyof Tabs]}
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]} chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
totalSeriesPrev={appConfigurator.totalSeries(keyName, true)}
totalSeries={appConfigurator.totalSeries(keyName)} totalSeries={appConfigurator.totalSeries(keyName)}
tableHeaderCells={tablesHeaders[keyName]} tableHeaderCells={tablesHeaders[keyName]}
/> />

View file

@ -18,4 +18,25 @@
flex-grow: 1; flex-grow: 1;
width: 100%; width: 100%;
} }
&-table {
&__header {
th:first-child {
width: 60%;
}
th:not(:first-child) {
width: auto;
}
}
&__progress {
display: grid;
grid-template-columns: minmax(200px, 1fr) 70px;
align-items: center;
justify-content: flex-start;
gap: $padding-small;
}
}
} }

View file

@ -4,6 +4,7 @@ export interface TSDBStatus {
totalSeries: number; totalSeries: number;
totalLabelValuePairs: number; totalLabelValuePairs: number;
totalSeriesByAll: number, totalSeriesByAll: number,
totalSeriesPrev: number,
seriesCountByMetricName: TopHeapEntry[]; seriesCountByMetricName: TopHeapEntry[];
seriesCountByLabelName: TopHeapEntry[]; seriesCountByLabelName: TopHeapEntry[];
seriesCountByFocusLabelValue: TopHeapEntry[]; seriesCountByFocusLabelValue: TopHeapEntry[];
@ -14,6 +15,8 @@ export interface TSDBStatus {
export interface TopHeapEntry { export interface TopHeapEntry {
name: string; name: string;
value: number; value: number;
diff: number;
valuePrev: number;
} }
interface QueryUpdaterArgs { interface QueryUpdaterArgs {

View file

@ -0,0 +1,26 @@
@use "src/styles/variables" as *;
.vm-dynamic-number {
font-size: $font-size-small;
color: $color-text-disabled;
&_positive {
color: $color-success;
}
&_negative {
color: $color-error;
}
&_down {
&:before {
content: "";
}
}
&_up {
&:before {
content: "";
}
}
}

View file

@ -10,6 +10,7 @@
@forward "./components/sectionheader"; @forward "./components/sectionheader";
@forward "./components/table"; @forward "./components/table";
@forward "./components/link"; @forward "./components/link";
@forward "./components/dynamic-number";
:root { :root {
/* base palette */ /* base palette */

View file

@ -25,6 +25,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: introduce `-http.maxConcurrentRequests` command-line flag to protect VM components from resource exhaustion during unexpected spikes of HTTP requests. By default, the new flag's value is set to 0 which means no limits are applied. * FEATURE: introduce `-http.maxConcurrentRequests` command-line flag to protect VM components from resource exhaustion during unexpected spikes of HTTP requests. By default, the new flag's value is set to 0 which means no limits are applied.
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add support for the different time formats for `--vm-native-filter-time-start` and `--vm-native-filter-time-end` flags if the native binary protocol is used for migration. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4091). * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add support for the different time formats for `--vm-native-filter-time-start` and `--vm-native-filter-time-end` flags if the native binary protocol is used for migration. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4091).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): integrate WITH template playground. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3811). * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): integrate WITH template playground. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3811).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add a comparison of data from the previous day with data from the current day to the `Cardinality Explorer`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3967).
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add ability to filter incoming requests by IP. See [these docs](https://docs.victoriametrics.com/vmauth.html#ip-filters) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3491). * FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add ability to filter incoming requests by IP. See [these docs](https://docs.victoriametrics.com/vmauth.html#ip-filters) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3491).
* BUGFIX: reduce the probability of sudden increase in the number of small parts on systems with small number of CPU cores. * BUGFIX: reduce the probability of sudden increase in the number of small parts on systems with small number of CPU cores.