Skip to content

Commit 2d7df4d

Browse files
committed
fix: per-tab 写锁修 popup 数字回落 + 加速整体检测链路
之前 detection / dynamic-snapshot / bundle-license / webRequest 这 4 路 writer 各自做 read-modify-write,慢的 writer 把过期快照写回时会把快的 writer 已经落盘的字段一起冲掉 —— 用户视觉上是 popup 检测数从 7→13→3→7 来回闪。新增 tab-write-lock.ts 维护一把 per-tab 异步写锁,所有 read-modify-write 段必须串行进入,后到的 writer 都能看到前一个 writer 刚落盘的最新数据。 接入位置:bundle / detection / dynamic-snapshot / index.ts 的 webRequest / message-router.ts 的 PAGE_DETECTION_RESULT 全部包到 withTabWriteLock 里;detection 拿到 page-detector 注入结果后才进锁,把 main fallback 的 fetch 放在锁外做,保持响应。popup-cache.ts 新增 mergePageDetectionRecord:同一 URL 下 page detection 重跑时合并旧的 technologies + 资源 URL,避免 React SPA 异步渲染过程中某次重扫抓的项变少导致 popup 数量临时回落。detection / message-router 写 data.page 改用合并而不是替换。 加速:dynamic-snapshot.ts DYNAMIC_SNAPSHOT_PROCESS_DELAY 800→400ms;content-observer.ts SEND_DELAY 之前也降到 400ms(在前面的 commit 里),配合 bundle 那侧 SCAN_DELAY / 并发优化,端到端从页面加载到 popup 完整识别从 ~8s 降到 ~2.4s。dynamic-snapshot 的 genericNames 同步跟 page-detector 那边的过滤名(search / sdk / analytics / pixel / ms.* 等)保持一致。clearTabDetectionState 里加 clearTabWriteLock 释放挂起锁条目。 将版本号提升到 1.3.60。
1 parent b149e04 commit 2d7df4d

7 files changed

Lines changed: 169 additions & 44 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stackprism",
33
"private": true,
4-
"version": "1.3.59",
4+
"version": "1.3.60",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

src/background/detection.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { augmentPageWithWordPressThemeStyles } from './wordpress'
2-
import { buildPopupCacheRecord, cleanPageDetectionRecord } from './popup-cache'
2+
import { buildPopupCacheRecord, cleanPageDetectionRecord, mergePageDetectionRecord } from './popup-cache'
33
import { fetchMainHeadersFallback, mergeHeaderRecords } from './headers'
44
import { clearBadge, clearTabSession, getTabData, getTabSnapshot, updateBadgeForTab, writeTabData } from './tab-store'
55
import { buildEffectivePageRules, loadDetectorSettings, loadTechRules } from './detector-settings'
66
import { scheduleBundleLicenseDetection } from './bundle-license'
77
import { injectContentObserver } from './content-injector'
8+
import { withTabWriteLock } from './tab-write-lock'
89
import { isDetectablePageUrl } from '@/utils/page-support'
910

1011
const activeDetectionTimers = new Map<number, ReturnType<typeof setTimeout>>()
@@ -106,7 +107,9 @@ export const runActivePageDetection = async (tabId: number, options: { force?: b
106107
lastDetectionRunAt.set(tabId, Date.now())
107108
console.log('[SP detection] run start', tabId, 'force:', Boolean(options.force))
108109
await injectContentObserver(tabId)
109-
const [data, rules, settings] = await Promise.all([getTabData(tabId), loadTechRules(), loadDetectorSettings()])
110+
// 这里不再预读 data —— page-detector 注入要 500ms+,期间其他 writer 会写过 storage;
111+
// 等 detector 跑完再统一 re-read 最新 data 再做合并写回
112+
const [rules, settings] = await Promise.all([loadTechRules(), loadDetectorSettings()])
110113
const pageRules = buildEffectivePageRules(rules.page || {}, settings)
111114
await chrome.scripting.executeScript({
112115
target: { tabId },
@@ -125,21 +128,36 @@ export const runActivePageDetection = async (tabId: number, options: { force?: b
125128
if (!page) return
126129

127130
const augmentedPage = await augmentPageWithWordPressThemeStyles(page)
128-
data.page = cleanPageDetectionRecord(augmentedPage)
129-
130-
if (needsMainHeadersFallback(data.main, (page as any).url || tab.url)) {
131-
const fallback = await fetchMainHeadersFallback((page as any).url || '', rules.headers || {}, settings)
132-
if (fallback) {
133-
data.main = shouldPreserveMainHeaderRecord(data.main, (page as any).url || tab.url)
134-
? mergeHeaderRecords(data.main, fallback)
135-
: fallback
136-
} else if (data.main && !shouldPreserveMainHeaderRecord(data.main, (page as any).url || tab.url)) {
137-
delete data.main
138-
}
131+
const freshClean = cleanPageDetectionRecord(augmentedPage)
132+
133+
// 进 per-tab 锁:read-modify-write 段必须跟其他 writer 串行,避免 dynamic/bundle/headers 并发读到同一份旧快照,
134+
// 互相把对方刚写好的字段覆盖掉(popup 上数字回落)
135+
let fallbackForMain: any = null
136+
const needsFallback = await withTabWriteLock(tabId, async () => {
137+
const peek = (await getTabData(tabId)) || {}
138+
return needsMainHeadersFallback(peek.main, (page as any).url || tab.url)
139+
})
140+
if (needsFallback) {
141+
fallbackForMain = await fetchMainHeadersFallback((page as any).url || '', rules.headers || {}, settings)
139142
}
140143

141-
data.updatedAt = Date.now()
142-
await saveTabDataAndBadge(tabId, data, settings)
144+
await withTabWriteLock(tabId, async () => {
145+
const latest = (await getTabData(tabId)) || {}
146+
latest.page = mergePageDetectionRecord(latest.page, freshClean)
147+
148+
if (needsMainHeadersFallback(latest.main, (page as any).url || tab.url)) {
149+
if (fallbackForMain) {
150+
latest.main = shouldPreserveMainHeaderRecord(latest.main, (page as any).url || tab.url)
151+
? mergeHeaderRecords(latest.main, fallbackForMain)
152+
: fallbackForMain
153+
} else if (latest.main && !shouldPreserveMainHeaderRecord(latest.main, (page as any).url || tab.url)) {
154+
delete latest.main
155+
}
156+
}
157+
158+
latest.updatedAt = Date.now()
159+
await saveTabDataAndBadge(tabId, latest, settings)
160+
})
143161
scheduleBundleLicenseDetection(tabId)
144162
} catch {
145163
return

src/background/dynamic-snapshot.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import { clearBadge, clearTabSession, getTabData, getTabSnapshot } from './tab-s
1313
import { saveTabDataAndBadge } from './detection'
1414
import { buildEffectivePageRules, loadDetectorSettings, loadTechRules } from './detector-settings'
1515
import { scheduleBundleLicenseDetection } from './bundle-license'
16+
import { withTabWriteLock } from './tab-write-lock'
1617

1718
const DYNAMIC_FAST_LOOKUP_RULE_MIN = 1000
18-
const DYNAMIC_SNAPSHOT_PROCESS_DELAY = 800
19+
const DYNAMIC_SNAPSHOT_PROCESS_DELAY = 400
1920

2021
const dynamicFrontendRuleKeyCache = new WeakMap()
2122
const dynamicFrontendHintsFlagCache = new WeakMap()
@@ -152,12 +153,29 @@ const isLikelyDynamicLibraryFileName = name => {
152153
'system',
153154
'systemjs',
154155
// 文档站 / 内容站常见的搜索 worker 文件名(mkdocs / docusaurus / vitepress 等都叫这名),
155-
// 真实的搜索库(Lunr / FlexSearch / Pagefind / Algolia)会通过专用规则或版权注释命中
156+
// 真实的搜索库(Lunr / FlexSearch / Pagefind / Algolia)会通过专用规则或官方版权注释命中
156157
'search',
158+
// 通用名,几乎所有站点都有但不属于公共库
159+
'sdk',
160+
'analytics',
161+
'tracker',
162+
'tracking',
163+
'beacon',
164+
'pixel',
157165
// 站点自身的内部脚本,不是公共库
158-
'tgwallpaper'
166+
'tgwallpaper',
167+
'jsbin'
159168
])
160-
return !genericNames.has(name.toLowerCase())
169+
if (genericNames.has(name.toLowerCase())) {
170+
return false
171+
}
172+
if (/^ms\.[a-z0-9_-]+$/i.test(name)) {
173+
return false
174+
}
175+
if (/^(?:tas-client|ethicalads|svg-loader)$/i.test(name)) {
176+
return false
177+
}
178+
return true
161179
}
162180

163181
const compileOptionalDynamicPattern = pattern => {
@@ -670,10 +688,15 @@ const processQueuedDynamicSnapshot = async tabId => {
670688
return
671689
}
672690

673-
const [data, rules, settings] = await Promise.all([getTabData(tabId), loadTechRules(), loadDetectorSettings()])
674-
data.dynamic = normalizeDynamicSnapshot(snapshot, buildEffectivePageRules(rules.page || {}, settings), data.dynamic)
675-
data.updatedAt = Date.now()
676-
await saveTabDataAndBadge(tabId, data, settings)
691+
const [rules, settings] = await Promise.all([loadTechRules(), loadDetectorSettings()])
692+
const pageRulesForDynamic = buildEffectivePageRules(rules.page || {}, settings)
693+
// 进 per-tab 锁:跟 detection / bundle / webRequest 串行做 read-modify-write,避免并发覆盖
694+
await withTabWriteLock(tabId, async () => {
695+
const latest = (await getTabData(tabId)) || {}
696+
latest.dynamic = normalizeDynamicSnapshot(snapshot, pageRulesForDynamic, latest.dynamic)
697+
latest.updatedAt = Date.now()
698+
await saveTabDataAndBadge(tabId, latest, settings)
699+
})
677700
scheduleBundleLicenseDetection(tabId)
678701
}
679702

src/background/index.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getTabData, getTabSnapshot } from './tab-store'
1313
import { SETTINGS_STORAGE_KEY, applyDetectorSettingsUpdate, loadDetectorSettings, loadTechRules } from './detector-settings'
1414
import { registerMessageRouter } from './message-router'
1515
import { clearBundleLicenseTimer } from './bundle-license'
16+
import { clearTabWriteLock, withTabWriteLock } from './tab-write-lock'
1617
import { isDetectablePageUrl, isObservableRequestUrl } from '@/utils/page-support'
1718

1819
registerMessageRouter()
@@ -41,6 +42,7 @@ const clearTabDetectionState = (tabId: number) => {
4142
clearBundleLicenseTimer(tabId)
4243
clearDynamicSnapshotTimer(tabId)
4344
clearPendingDynamicSnapshot(tabId)
45+
clearTabWriteLock(tabId)
4446
clearBadge(tabId)
4547
clearTabSession(tabId).catch(() => {})
4648
}
@@ -105,8 +107,8 @@ chrome.webRequest.onHeadersReceived.addListener(
105107
if (details.tabId < 0 || !details.responseHeaders) return
106108
if (!isObservableRequestUrl(details.url)) return
107109

108-
Promise.all([getTabData(details.tabId), loadTechRules(), loadDetectorSettings(), getTabSnapshot(details.tabId)])
109-
.then(([data, rules, settings, tab]) => {
110+
Promise.all([loadTechRules(), loadDetectorSettings(), getTabSnapshot(details.tabId)])
111+
.then(async ([rules, settings, tab]) => {
110112
if (details.type === 'main_frame' && !isDetectablePageUrl(details.url)) {
111113
clearTabDetectionState(details.tabId)
112114
return
@@ -116,18 +118,22 @@ chrome.webRequest.onHeadersReceived.addListener(
116118
return
117119
}
118120
const record = buildHeaderRecord(details, rules.headers || {}, settings)
119-
if (details.type === 'main_frame') {
120-
clearCrossOriginDynamicSnapshot(data, details.url)
121-
data.main = shouldMergeHeaderRecords(data.main, record) ? mergeHeaderRecords(data.main, record) : record
122-
data.apis = []
123-
data.frames = []
124-
} else if (details.type === 'xmlhttprequest' || (details.type as string) === 'fetch' || details.type === 'websocket') {
125-
data.apis = dedupeApiRecords([record, ...(data.apis || [])])
126-
} else if (details.type === 'sub_frame') {
127-
data.frames = dedupeApiRecords([record, ...(data.frames || [])]).slice(0, 10)
128-
}
129-
data.updatedAt = Date.now()
130-
return saveTabDataAndBadge(details.tabId, data, settings)
121+
// 进 per-tab 锁:concurrent webRequest 事件不能并发 read-modify-write,否则会互相覆盖彼此的 apis / frames / main
122+
await withTabWriteLock(details.tabId, async () => {
123+
const latest = (await getTabData(details.tabId)) || {}
124+
if (details.type === 'main_frame') {
125+
clearCrossOriginDynamicSnapshot(latest, details.url)
126+
latest.main = shouldMergeHeaderRecords(latest.main, record) ? mergeHeaderRecords(latest.main, record) : record
127+
latest.apis = []
128+
latest.frames = []
129+
} else if (details.type === 'xmlhttprequest' || (details.type as string) === 'fetch' || details.type === 'websocket') {
130+
latest.apis = dedupeApiRecords([record, ...(latest.apis || [])])
131+
} else if (details.type === 'sub_frame') {
132+
latest.frames = dedupeApiRecords([record, ...(latest.frames || [])]).slice(0, 10)
133+
}
134+
latest.updatedAt = Date.now()
135+
await saveTabDataAndBadge(details.tabId, latest, settings)
136+
})
131137
})
132138
.catch(() => {})
133139
},

src/background/message-router.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ import { augmentPageWithWordPressThemeStyles, detectWordPressThemeStylesFromPage
33
import { clearBadge, clearTabSession, getTabData, getTabSnapshot } from './tab-store'
44
import { queueDynamicSnapshot } from './dynamic-snapshot'
55
import { addStoredCustomHeaderRules } from './headers'
6-
import { buildPopupRawResult, cleanPageDetectionRecord, cleanTechnologyRecords, getPopupResultResponse } from './popup-cache'
6+
import {
7+
buildPopupRawResult,
8+
cleanPageDetectionRecord,
9+
cleanTechnologyRecords,
10+
getPopupResultResponse,
11+
mergePageDetectionRecord
12+
} from './popup-cache'
713
import { runActivePageDetection, saveTabDataAndBadge } from './detection'
814
import { loadDetectorSettings } from './detector-settings'
15+
import { withTabWriteLock } from './tab-write-lock'
916
import { checkPageSupport, isDetectablePageUrl } from '@/utils/page-support'
1017

1118
const clearUnsupportedTab = async (tabId: number) => {
@@ -115,16 +122,21 @@ export const registerMessageRouter = () => {
115122
sendResponse({ ok: false, error: '缺少有效 tabId' })
116123
return false
117124
}
118-
Promise.all([getTabData(tabId), loadDetectorSettings(), getTabSnapshot(tabId)])
119-
.then(async ([data, settings, tab]) => {
125+
Promise.all([loadDetectorSettings(), getTabSnapshot(tabId)])
126+
.then(async ([settings, tab]) => {
120127
if (!isDetectablePageUrl(tab.url)) {
121128
await clearUnsupportedTab(tabId)
122129
return
123130
}
124131
const page = await augmentPageWithWordPressThemeStyles(message.page)
125-
data.page = cleanPageDetectionRecord(page)
126-
data.updatedAt = Date.now()
127-
return saveTabDataAndBadge(tabId, data, settings)
132+
const freshClean = cleanPageDetectionRecord(page)
133+
// 进 per-tab 锁:跟 detection / dynamic / bundle / webRequest 串行,避免互相覆盖字段
134+
await withTabWriteLock(tabId, async () => {
135+
const latest = (await getTabData(tabId)) || {}
136+
latest.page = mergePageDetectionRecord(latest.page, freshClean)
137+
latest.updatedAt = Date.now()
138+
await saveTabDataAndBadge(tabId, latest, settings)
139+
})
128140
})
129141
.then(() => sendResponse({ ok: true }))
130142
.catch(error => sendResponse({ ok: false, error: String(error) }))

src/background/popup-cache.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,3 +522,39 @@ export const cleanPageDetectionRecord = (page: any) => ({
522522
technologies: cleanTechnologyRecords(page?.technologies),
523523
resources: cleanPageResources(page?.resources)
524524
})
525+
526+
// 同一 URL 下 page detection 多次重跑(SPA 异步渲染、tab 复用、用户在页面停留期间触发再扫描)时
527+
// 合并技术列表 + 资源 URL,而不是直接整段替换;可以避免新一轮抓得少导致 popup 上检测项闪烁式回落
528+
export const mergePageDetectionRecord = (previous: any, fresh: any) => {
529+
if (!fresh) return previous || null
530+
if (!previous || !previous.url || previous.url !== fresh.url) return fresh
531+
const previousTechs = Array.isArray(previous.technologies) ? previous.technologies : []
532+
const freshTechs = Array.isArray(fresh.technologies) ? fresh.technologies : []
533+
const previousResources = previous.resources || {}
534+
const freshResources = fresh.resources || {}
535+
const mergeUrlList = (a: any, b: any, limit: number) => {
536+
const seen = new Set<string>()
537+
const out: string[] = []
538+
for (const value of [...(Array.isArray(a) ? a : []), ...(Array.isArray(b) ? b : [])]) {
539+
const url = typeof value === 'string' ? value : ''
540+
if (!url || seen.has(url)) continue
541+
seen.add(url)
542+
out.push(url)
543+
if (out.length >= limit) break
544+
}
545+
return out
546+
}
547+
return {
548+
...fresh,
549+
technologies: mergeTechnologyRecords([...previousTechs, ...freshTechs]),
550+
resources: {
551+
...previousResources,
552+
...freshResources,
553+
scripts: mergeUrlList(previousResources.scripts, freshResources.scripts, 200),
554+
stylesheets: mergeUrlList(previousResources.stylesheets, freshResources.stylesheets, 200),
555+
resourceTiming: mergeUrlList(previousResources.resourceTiming, freshResources.resourceTiming, 400),
556+
all: mergeUrlList(previousResources.all, freshResources.all, 400),
557+
themeAssetUrls: mergeUrlList(previousResources.themeAssetUrls, freshResources.themeAssetUrls, 120)
558+
}
559+
}
560+
}

src/background/tab-write-lock.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// 每个 tab 共用一把异步写锁:detection / dynamic / bundle / webRequest 这些并发 writer
2+
// 必须串行化进入 read-modify-write 段,否则它们各自读到的快照可能没包含彼此的最新写入,
3+
// 互相把对方的字段覆盖掉(用户视觉:popup 上数字 7 → 13 → 3 → 7 来回闪)
4+
const tabWriteLocks = new Map<number, Promise<void>>()
5+
6+
export const withTabWriteLock = async <T>(tabId: number, task: () => Promise<T>): Promise<T> => {
7+
if (typeof tabId !== 'number' || tabId < 0) return task()
8+
const previous = tabWriteLocks.get(tabId) || Promise.resolve()
9+
let release: () => void = () => {}
10+
const next = new Promise<void>(resolve => {
11+
release = resolve
12+
})
13+
tabWriteLocks.set(
14+
tabId,
15+
previous.then(() => next)
16+
)
17+
try {
18+
await previous
19+
return await task()
20+
} finally {
21+
release()
22+
if (tabWriteLocks.get(tabId) === next) {
23+
tabWriteLocks.delete(tabId)
24+
}
25+
}
26+
}
27+
28+
export const clearTabWriteLock = (tabId: number): void => {
29+
tabWriteLocks.delete(tabId)
30+
}

0 commit comments

Comments
 (0)