трейсы

This commit is contained in:
gorbenko.pavel
2026-08-20 17:10:54 +05:00
parent c6c549eae9
commit a4e6e097f3
3 changed files with 385 additions and 83 deletions
@@ -439,13 +439,13 @@ async function getNavCountsInner(context: WebPartContext): Promise<INavCounts> {
const [turn, rework] = await Promise.all([
sharedPendingIndex(context, 'myTurn', MY_TURN_STATUSES),
getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1),
countOwnRequestsByStatus(context, types, REQUEST_STATUS.returned),
]);
return {
approvals: turn.index.filter((r) => r.myStepStatus === REQUEST_STATUS.onApproval).length,
execution: turn.index.filter((r) => r.myStepStatus === REQUEST_STATUS.onExecution).length,
rework: rework.totalRows,
rework,
};
}
@@ -727,14 +727,23 @@ async function finalizeApprovalTarget(
// Заголовок/статус/дата заявки — отдельным запросом к списку ДАННЫХ (см.
// комментарий у функции: через Lookup-expand это не отдаётся).
//
// Автор берётся ЗДЕСЬ ЖЕ через expand. Раньше он дочитывался отдельным
// проходом (hydrateAuthors) и только по строкам видимой страницы — чтобы не
// тянуть его на весь индекс. Но экономилась при этом не сеть, а payload:
// запрос к списку данных всё равно делается, и expand не добавляет к нему
// ни одного round-trip'а, тогда как отдельный проход стоил целый — замер
// 2026-08-20 показал 262 мс ровно на критическом пути вкладки
// «На согласовании» (Search → строки очереди → автор).
const ticketFilter = ticketIds.map((id) => `Id eq ${id}`).join(' or ');
const ticketRows = (await list.items
.filter(ticketFilter)
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created')
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
.expand('Author')
.top(ticketIds.length)()) as Array<Record<string, unknown>>;
debug.rows = ticketRows.map((r) => ({
...toMyRequest(r, undefined, type, 'approval', target),
...toMyRequest(r, r.Author as { EMail?: string; Title?: string } | undefined, type, 'approval', target),
myStepStatus: stepStatusByTicket.get(r.Id as number),
}));
@@ -749,16 +758,23 @@ async function finalizeApprovalTarget(
}
}
// Дочитывает Author/EMail + Author/Title в списке ДАННЫХ каждого типа —
// только по переданным строкам (обычно уже нарезанная страница из ≤20), не по
// всему найденному срезу. Мутирует переданные объекты (проставляет authorEmail/authorName).
// Дочитывает Author/EMail + Author/Title в списке ДАННЫХ каждого типа — только
// по переданным строкам. Мутирует переданные объекты (проставляет
// authorEmail/authorName).
//
// Строки, у которых автор УЖЕ известен, пропускаются: и 'own' (loadByIds), и
// 'approval' (finalizeApprovalTarget) теперь получают его тем же запросом, что
// и остальные поля, поэтому на обычных путях сюда доходить нечему и запрос не
// уходит вовсе. Функция оставлена страховкой на случай источника строк без
// автора — молча показывать пустого инициатора хуже, чем один лишний запрос.
async function hydrateAuthors(
context: WebPartContext,
types: IRequestType[],
rows: IMyRequest[]
): Promise<void> {
if (rows.length === 0) return;
return traced('дочитывание автора (REST)', () => hydrateAuthorsInner(context, types, rows));
const missing = rows.filter((r) => !r.authorName && !r.authorEmail);
if (missing.length === 0) return;
return traced('дочитывание автора (REST)', () => hydrateAuthorsInner(context, types, missing));
}
async function hydrateAuthorsInner(
@@ -862,6 +878,10 @@ async function fetchAllSearchRows(
let startRow = 0;
for (;;) {
// Постраничной метки здесь больше нет: замер 2026-08-20 показал, что запрос
// ровно один (847 мс у вызывающего против 846 мс у страницы), то есть
// медленный сам Search, а не перебор StartRow. Вопрос закрыт, а метка
// занимала место в строке сводки, дублируя метку вызывающего.
const results = await sp.search({
Querytext: kql,
RowLimit: SEARCH_PAGE_SIZE,
@@ -1310,6 +1330,36 @@ export async function getOwnRequestsByStatus(
status: string,
page: number,
query: string = ''
): Promise<IOwnRequestsPage> {
return ownRequestsByStatus(context, types, status, page, query, true);
}
/**
* Только СКОЛЬКО таких заявок — для счётчика «Доработка» в боковом меню.
*
* Отличие от getOwnRequestsByStatus одно: не дочитываются поля найденных заявок
* (loadByIds). Счётчику они не нужны — ему нужен totalRows, — а замер 2026-08-20
* показал, что этот шаг стоит ~200-300 мс и идёт СТРОГО ПОСЛЕ Search, то есть
* целиком лежит на критическом пути открытия портала.
*
* Условия отбора и довесок по свежести — те же самые, поэтому счётчик и вкладка
* по-прежнему не расходятся (ради чего они и считаются одним кодом).
*/
export async function countOwnRequestsByStatus(
context: WebPartContext,
types: IRequestType[],
status: string
): Promise<number> {
return (await ownRequestsByStatus(context, types, status, 1, '', false)).totalRows;
}
async function ownRequestsByStatus(
context: WebPartContext,
types: IRequestType[],
status: string,
page: number,
query: string,
withRows: boolean
): Promise<IOwnRequestsPage> {
if (types.length === 0) return { rows: [], totalRows: 0 };
@@ -1339,7 +1389,7 @@ export async function getOwnRequestsByStatus(
const totalRows = results.TotalRows || 0;
const entries = hitsToEntries(results, types);
const byKey =
entries.length > 0
withRows && entries.length > 0
? await traced('дочитывание полей по Id (REST)', () => loadByIds(context, entries, 'own'))
: new Map<string, IMyRequest>();
const searchRows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r);
@@ -1357,7 +1407,10 @@ export async function getOwnRequestsByStatus(
.reduce((acc, r) => acc.concat(r), [] as IMyRequest[])
.filter((r) => r.status === status);
const searchKeys = new Set(searchRows.map((r) => rowKey(r)));
// Ключи для дедупа берём из ХИТОВ индекса, а не из дочитанных строк: пара
// «партиция + Id» известна уже по ответу Search, и режиму счётчика (withRows
// = false) не приходится ради этого дочитывать поля.
const searchKeys = new Set(entries.map((e) => requestKey(e.target.listId, e.id)));
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
const merged = onlyFresh