From 7bd540e8164eb572c9c57874cd2511beecd0b0db Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 19 Aug 2026 16:36:10 +0500 Subject: [PATCH] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D1=84=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/myRequests/RequestsTableView.tsx | 3 +- .../features/myRequests/SearchView.tsx | 3 +- .../features/myRequests/myRequests.service.ts | 141 ++++++++---------- .../features/myRequests/perfTrace.ts | 21 --- 4 files changed, 63 insertions(+), 105 deletions(-) delete mode 100644 src/webparts/requestPortal/features/myRequests/perfTrace.ts diff --git a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx index a8f9988..9aa8f71 100644 --- a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx +++ b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx @@ -16,7 +16,6 @@ import { IMyRequest } from './models/IMyRequest'; import { DataTable, IDataTableColumn } from '@tnsu/ui-kit'; import { REQUEST_STATUS, REQUEST_STATUS_CHOICES, IRequestType, StatusBadge } from '../../shared/approval'; import { onRequestsChanged } from '../../shared/requestEvents'; -import { traced } from './perfTrace'; import { useRoutedQuery } from '../../shared/usePersistedQuery'; import { IPage, pageKey, pageSignature, readPage, writePage } from './pageCache'; import { Pager } from './Pager'; @@ -399,7 +398,7 @@ export const RequestsTableView: React.FC = ({ const load = async (forceRender = false): Promise => { try { - const fresh = await traced(`ИТОГО таблица:${view}#${page}`, fetchPage); + const fresh = await fetchPage(); if (cancelled) return; // Перерисовываем, только если данные реально изменились — иначе при diff --git a/src/webparts/requestPortal/features/myRequests/SearchView.tsx b/src/webparts/requestPortal/features/myRequests/SearchView.tsx index 52ff9e9..0411d1a 100644 --- a/src/webparts/requestPortal/features/myRequests/SearchView.tsx +++ b/src/webparts/requestPortal/features/myRequests/SearchView.tsx @@ -4,7 +4,6 @@ import { getLinkedTypes, hydratePendingAuthors, searchMyRequests } from './myReq import { IMyRequest } from './models/IMyRequest'; import { IRequestType, StatusBadge } from '../../shared/approval'; import { onRequestsChanged } from '../../shared/requestEvents'; -import { traced } from './perfTrace'; import { useRoutedQuery } from '../../shared/usePersistedQuery'; import { pageKey, pageSignature, readPage, writePage } from './pageCache'; import { Pager } from './Pager'; @@ -186,7 +185,7 @@ export const SearchView: React.FC = ({ const load = async (forceRender = false): Promise => { try { - const fresh = await traced('ИТОГО поиск', () => searchMyRequests(context, types, debouncedQuery)); + const fresh = await searchMyRequests(context, types, debouncedQuery); if (cancelled) return; applyKnownAuthors(fresh.rows, authors.current); const prev = readPage(cacheKey); diff --git a/src/webparts/requestPortal/features/myRequests/myRequests.service.ts b/src/webparts/requestPortal/features/myRequests/myRequests.service.ts index 5fa1351..05c3d96 100644 --- a/src/webparts/requestPortal/features/myRequests/myRequests.service.ts +++ b/src/webparts/requestPortal/features/myRequests/myRequests.service.ts @@ -16,7 +16,6 @@ import { effectiveUserEmail } from '../../shared/impersonation'; import { IMyRequest } from './models/IMyRequest'; import { IMyRequestsTypeDebug } from './models/IMyRequestsDebug'; import { traceLinkedTypes, traceResultRows, traceSearchHits } from './searchTrace'; -import { traced } from './perfTrace'; // Список данных «... — данные» → список согласования «... — согласование» // (конвенция именования; см. forms/*/provisioning.json и forms/*/approval/provisioning.json). @@ -203,9 +202,7 @@ async function buildPendingIndex( // полноценный Search, который эти вызовы как раз заменяют. const resolved = await Promise.all( flat.map(({ type, target }) => - traced(`resolveApprovalSearchTarget:${type.name}#${target.listId}`, () => - resolveApprovalSearchTarget(context, type, target) - ) + resolveApprovalSearchTarget(context, type, target) ) ); @@ -228,24 +225,22 @@ async function buildPendingIndex( // поэтому идут параллельно, а не один за другим. const [hitsByTarget, recentByTarget] = await Promise.all([ okEntries.length > 0 - ? traced('search:approvalsAcrossAll', () => searchAcrossApprovalLists(context, okEntries, tokenEmail, statuses)) + ? searchAcrossApprovalLists(context, okEntries, tokenEmail, statuses) : Promise.resolve(new Map()), // Довесок нужен только режиму «чья очередь сейчас»: при текстовом поиске // важна полнота по архиву, а не мгновенная свежесть (см. scope='involved'). okEntries.length > 0 && scope === 'myTurn' - ? traced('recentRowsAcrossAll', () => fetchRecentApprovalRows(context, okEntries, tokenEmail, statuses)) + ? fetchRecentApprovalRows(context, okEntries, tokenEmail, statuses) : Promise.resolve(new Map>>()), ]); const parts = await Promise.all( okEntries.map((entry) => - traced(`finalizeApprovalTarget:${entry.type.name}#${entry.target.listId}`, () => - finalizeApprovalTarget( - context, - entry, - hitsByTarget.get(targetKey(entry)) ?? [], - recentByTarget.get(targetKey(entry)) ?? [] - ) + finalizeApprovalTarget( + context, + entry, + hitsByTarget.get(targetKey(entry)) ?? [], + recentByTarget.get(targetKey(entry)) ?? [] ) ) ); @@ -343,15 +338,15 @@ export interface INavCounts { // своим запросом — но параллельно, и ровно тем же вызовом, что наполняет саму // вкладку: иначе счётчик и список могли бы разойтись. export async function getNavCounts(context: WebPartContext): Promise { - return traced('ИТОГО getNavCounts', () => getNavCountsInner(context)); + return getNavCountsInner(context); } async function getNavCountsInner(context: WebPartContext): Promise { const types = await getLinkedTypes(context); const [turn, rework] = await Promise.all([ - traced('buildPendingIndex:myTurn', () => buildPendingIndex(context, 'myTurn', MY_TURN_STATUSES)), - traced('getOwnRequestsByStatus:rework', () => getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1)), + buildPendingIndex(context, 'myTurn', MY_TURN_STATUSES), + getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1), ]); return { @@ -419,7 +414,7 @@ async function resolveApprovalSearchTarget( const dataTitle = target.title ?? - ((await traced(`dataListTitle:${type.name}#${target.listId}`, () => list.select('Title')())).Title as string) ?? + ((await list.select('Title')()).Title as string) ?? ''; if (!DATA_LIST_SUFFIX.test(dataTitle)) { return { @@ -432,9 +427,9 @@ async function resolveApprovalSearchTarget( const approvalListTitle = `${dataTitle.replace(DATA_LIST_SUFFIX, '')} — согласование`; const approvalList = sp.web.lists.getByTitle(approvalListTitle); - const rootFolder = (await traced(`approvalListUrl:${type.name}#${target.listId}`, () => - approvalList.select('RootFolder/ServerRelativeUrl').expand('RootFolder')() - )) as { RootFolder?: { ServerRelativeUrl?: string } }; + const rootFolder = (await approvalList.select('RootFolder/ServerRelativeUrl').expand('RootFolder')()) as { + RootFolder?: { ServerRelativeUrl?: string }; + }; const relUrl = rootFolder.RootFolder?.ServerRelativeUrl; if (!relUrl) { return { kind: 'error', type, target, message: `Список согласования «${approvalListTitle}» не найден` }; @@ -583,7 +578,7 @@ async function fetchRecentApprovalRows( // SharePoint REST — поэтому резолвим email в числовой Id (как и везде в // проекте) и фильтруем по TansuSiteApproverId. const claimsLogin = tokenEmail.indexOf('|') >= 0 ? tokenEmail : `i:0#.f|membership|${tokenEmail}`; - const approver = await traced(`ensureUser:${siteUrl}`, () => sp.web.ensureUser(claimsLogin)); + const approver = await sp.web.ensureUser(claimsLogin); const filter = `TansuSiteApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'${statusFilter}`; const [batchedSP, execute] = sp.batched(); @@ -609,7 +604,7 @@ async function fetchRecentApprovalRows( }) ); - await traced(`recentRowsBatch:${siteUrl} (партиций: ${siteEntries.length})`, execute); + await execute(); await Promise.all(pending); } catch (e) { console.error(`[Текущие заявки] Довесок по свежести для ${siteUrl} не сработал:`, (e as Error).message); @@ -662,12 +657,10 @@ async function finalizeApprovalTarget( // Id режем на пачки — тот же предел длины URL REST $filter, что и в // loadByIds (см. REST_ID_FILTER_CHUNK). chunk(searchHitIds, REST_ID_FILTER_CHUNK).map((idsChunk) => - traced(`approvalRowsRest:${type.name}#${target.listId}`, () => - approvalList.items - .filter(idsChunk.map((id) => `Id eq ${id}`).join(' or ')) - .select('Id', 'TicketId', 'ApprovalStatus') - .top(idsChunk.length)() - ) + approvalList.items + .filter(idsChunk.map((id) => `Id eq ${id}`).join(' or ')) + .select('Id', 'TicketId', 'ApprovalStatus') + .top(idsChunk.length)() ) ) ).reduce((all, r) => all.concat(r as Array>), [] as Array>); @@ -702,12 +695,10 @@ async function finalizeApprovalTarget( // Заголовок/статус/дата заявки — отдельным запросом к списку ДАННЫХ (см. // комментарий у функции: через Lookup-expand это не отдаётся). const ticketFilter = ticketIds.map((id) => `Id eq ${id}`).join(' or '); - const ticketRows = (await traced(`ticketRowsRest:${type.name}#${target.listId}`, () => - list.items - .filter(ticketFilter) - .select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created') - .top(ticketIds.length)() - )) as Array>; + const ticketRows = (await list.items + .filter(ticketFilter) + .select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created') + .top(ticketIds.length)()) as Array>; debug.rows = ticketRows.map((r) => ({ ...toMyRequest(r, undefined, type, 'approval', target), @@ -831,16 +822,14 @@ async function fetchAllSearchRows( let startRow = 0; for (;;) { - const results = await traced(`search:${label}#${startRow}`, () => - sp.search({ - Querytext: kql, - RowLimit: SEARCH_PAGE_SIZE, - StartRow: startRow, - SelectProperties: selectProperties, - TrimDuplicates: false, - ...(sortList ? { SortList: sortList } : {}), - }) - ); + const results = await sp.search({ + Querytext: kql, + RowLimit: SEARCH_PAGE_SIZE, + StartRow: startRow, + SelectProperties: selectProperties, + TrimDuplicates: false, + ...(sortList ? { SortList: sortList } : {}), + }); const page = results.PrimarySearchResults as Array>; rows.push(...page); @@ -1080,13 +1069,11 @@ async function loadByIds( await Promise.all( chunk(ids, REST_ID_FILTER_CHUNK).map(async (idsChunk) => { - const rows = (await traced(`loadByIds:${kind}:${type.name}#${target.listId}`, () => - list.items - .filter(idsChunk.map((id) => `Id eq ${id}`).join(' or ')) - .select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title') - .expand('Author') - .top(idsChunk.length)() - )) as Array>; + const rows = (await list.items + .filter(idsChunk.map((id) => `Id eq ${id}`).join(' or ')) + .select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title') + .expand('Author') + .top(idsChunk.length)()) as Array>; rows.forEach((r) => { const author = r.Author as { EMail?: string; Title?: string } | undefined; byKey.set( @@ -1205,13 +1192,13 @@ export async function getOwnRequests( ): Promise { if (types.length === 0) return { rows: [], totalRows: 0 }; - const searchPage = await traced('searchOwnRequests', () => searchOwnRequests(context, types, '', page)); + const searchPage = await searchOwnRequests(context, types, '', page); if (page !== 1) return searchPage; const freshRows = ( await Promise.all( - types.map((t) => traced(`getFreshOwnRowsForType:${t.name}`, () => getFreshOwnRowsForType(context, t))) + types.map((t) => getFreshOwnRowsForType(context, t)) ) ).reduce((all, r) => all.concat(r), [] as IMyRequest[]); @@ -1248,23 +1235,21 @@ export async function searchOwnRequests( if (text) clauses.push(text); const kql = clauses.join(' AND '); - const results = await traced('search:searchOwnRequests', () => - sp.search({ - Querytext: kql, - RowLimit: PAGE_SIZE, - StartRow: (page - 1) * PAGE_SIZE, - SelectProperties: ['ListItemID', 'Title', 'Path'], - TrimDuplicates: false, - // Свежие сверху (по умолчанию Search сортирует по релевантности, не по дате). - SortList: [{ Property: 'Created', Direction: 1 }], - }) - ); + const results = await sp.search({ + Querytext: kql, + RowLimit: PAGE_SIZE, + StartRow: (page - 1) * PAGE_SIZE, + SelectProperties: ['ListItemID', 'Title', 'Path'], + TrimDuplicates: false, + // Свежие сверху (по умолчанию Search сортирует по релевантности, не по дате). + SortList: [{ Property: 'Created', Direction: 1 }], + }); const totalRows = results.TotalRows || 0; const entries = hitsToEntries(results, types); const byKey = entries.length > 0 - ? await traced('loadByIds:own(searchOwnRequests)', () => loadByIds(context, entries, 'own')) + ? await loadByIds(context, entries, 'own') : new Map(); // Порядок задаёт релевантность поиска, а не порядок ответов REST. const rows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r); @@ -1299,23 +1284,21 @@ export async function getOwnRequestsByStatus( if (text) clauses.push(text); const kql = clauses.join(' AND '); - const results = await traced('search:getOwnRequestsByStatus', () => - sp.search({ - Querytext: kql, - RowLimit: PAGE_SIZE, - StartRow: (page - 1) * PAGE_SIZE, - SelectProperties: ['ListItemID', 'Title', 'Path'], - TrimDuplicates: false, - // Свежие сверху — как в searchOwnRequests. - SortList: [{ Property: 'Created', Direction: 1 }], - }) - ); + const results = await sp.search({ + Querytext: kql, + RowLimit: PAGE_SIZE, + StartRow: (page - 1) * PAGE_SIZE, + SelectProperties: ['ListItemID', 'Title', 'Path'], + TrimDuplicates: false, + // Свежие сверху — как в searchOwnRequests. + SortList: [{ Property: 'Created', Direction: 1 }], + }); const totalRows = results.TotalRows || 0; const entries = hitsToEntries(results, types); const byKey = entries.length > 0 - ? await traced('loadByIds:own(getOwnRequestsByStatus)', () => loadByIds(context, entries, 'own')) + ? await loadByIds(context, entries, 'own') : new Map(); const searchRows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r); @@ -1326,9 +1309,7 @@ export async function getOwnRequestsByStatus( // вернуть на доработку только что, индекс мог не успеть её подхватить. const freshRows = ( await Promise.all( - types.map((t) => - traced(`getRecentlyChangedOwnRowsForType:${t.name}`, () => getRecentlyChangedOwnRowsForType(context, t)) - ) + types.map((t) => getRecentlyChangedOwnRowsForType(context, t)) ) ) .reduce((acc, r) => acc.concat(r), [] as IMyRequest[]) diff --git a/src/webparts/requestPortal/features/myRequests/perfTrace.ts b/src/webparts/requestPortal/features/myRequests/perfTrace.ts deleted file mode 100644 index 0d78e69..0000000 --- a/src/webparts/requestPortal/features/myRequests/perfTrace.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Замер времени отдельных этапов загрузки «Моих заявок» — что именно тормозит - * (индекс согласований, свои заявки по типу, конкретная партиция, Search), - * а не просто «страница долго грузится». - * - * ВРЕМЕННО: пишет всегда, без флага-переключателя — этот модуль и все traced() - * вокруг снимаются целиком, как только разбор текущей проблемы с - * производительностью закончится (см. myRequests.service.ts, RequestsTableView.tsx, - * SearchView.tsx). - */ -const PREFIX = '[Перф]'; - -/** Оборачивает async-шаг замером времени. */ -export async function traced(label: string, fn: () => Promise): Promise { - const start = performance.now(); - try { - return await fn(); - } finally { - console.log(`${PREFIX} ${label}: ${(performance.now() - start).toFixed(0)} мс`); - } -}