package main import ( "encoding/base64" "io" "net/http" "strconv" "sync" ) // ===== 页映射紧凑索引 (分块懒加载的数据源) ===== // // 旧实现 (rPages) 每被调用一次就要: // 1. 同步等全库审计算完 (要扫 19.5 万页); // 2. 逐页 readPage, 造 19.5 万个 PageItem 结构体; // 3. 再造两张 19.5 万键的 map (role/owner, 键还是页号的十进制字符串); // 4. 把这三样一次性 json 序列化 —— 单次响应几十 MB。 // // 而概览页 / 堆链页 / 空闲页三个视图各会调一次 /api/pages, 前端拿到后还要把 // 19.5 万个
拼成一个巨型字符串塞进 innerHTML, 于是浏览器卡死甚至崩溃。 // // 现在改成: 全库只建一次紧凑索引 —— 每页 1 字节类型 + 1 字节使用率, // 19.5 万页也只占 ~390KB 内存; 接口按块 (base64 字节串) 返回。 // 前端分块拉取 + canvas 虚拟化渲染, 只画视口内的格子, DOM 里永远只有一块画布。 const ( pmChunkDefault = 20000 // 单块默认页数 (base64 后约 54KB) pmChunkMax = 65536 pmNotLoaded = -9 // "该页数据还没拉到前端" 的哨兵 (仅前端使用) ) // pageIndex 全库页映射的紧凑表示。 type pageIndex struct { total int typ []int8 // 下标 = 页号 (1..total); 0 号槽不用。取值见 PG_* 常量 usg []uint8 // 使用率百分比 0..100 counts map[string]int allocated int neverUsed int firstFree uint32 } var ( pmMu sync.Mutex // 保护 pmIdx / pmGen pmBuildMu sync.Mutex // 串行化构建: 并发请求只算一次 pmIdx *pageIndex pmGen uint64 ) var pmTypeNames = map[int]string{ 0: "free", 1: "table", 2: "data", 3: "heap", 4: "imeta", 5: "idata", 6: "overflow", 7: "wal", 8: "sext", 9: "blob", } // pmKind 归一化页类型: 返回 (给前端着色的类型值, counts 用的键名)。 // 未知类型字节统一折叠成 PG_OTHER, 免得前端拿到没有色号的取值。 // 注意与 audit.go 的 pageTypeName 区分: 那个返回的是 "Free"/"Table" 这类展示名, // 且不认识本文件这几个哨兵; 这里的键名要和旧 rPages 的 counts 保持一致。 func pmKind(pt int) (int, string) { switch pt { case PG_RESERVED: return pt, "reserved" case PG_NEVER: return pt, "never" case PG_ORPHAN: return pt, "orphan" } if n, ok := pmTypeNames[pt]; ok { return pt, n } return PG_OTHER, "other" } func pmNewCounts() map[string]int { return map[string]int{ "hdr": 0, "table": 0, "data": 0, "heap": 0, "imeta": 0, "idata": 0, "overflow": 0, "wal": 0, "sext": 0, "blob": 0, "free": 0, "never": 0, "reserved": 0, "orphan": 0, "other": 0, } } // buildPageIndex 单趟扫描全库, 生成紧凑页索引。这是唯一一次整库读取。 // // 刻意**不依赖全库审计**: 页图需要的只有头部 (NextFreePage) 和空闲链, 而这两样 // 在 openH 里就已经就绪 (rHdr + rFreeChain)。审计在大表上要跑一两分钟, 若页图 // 跟着一起等, 用户打开库后半天出不来图 —— 现在解耦后几秒就能出图, 且分块返回。 func buildPageIndex() *pageIndex { idx := &pageIndex{counts: pmNewCounts()} numPages := totalPages() idx.total = numPages if numPages <= 0 { return idx } // 页号约定与 runAudit 完全一致 (见 audit.go): 0=header, 1=预留, 分配从 2 开始。 nf := le32(hdr[12:16]) idx.firstFree = nf if nf > 2 { idx.allocated = int(nf) - 2 } if idx.allocated > numPages { idx.allocated = numPages } reserved := 1 if idx.neverUsed = numPages - idx.allocated - reserved; idx.neverUsed < 0 { idx.neverUsed = 0 } firstFree := nf if firstFree == 0 { firstFree = uint32(numPages) + 1 } freeSet := map[uint32]bool{} if fi := rFreeChain(); fi != nil { freeSet = make(map[uint32]bool, fi.Total) for _, p := range fi.Pages { freeSet[p] = true } } idx.typ = make([]int8, numPages+1) idx.usg = make([]uint8, numPages+1) f, e := dbFile() if e != nil { return idx } // 顺序批量读: 一次 512 页 (8MB)。逐页 ReadAt 要 17.7 万次系统调用 + 17.7 万次 // 16KB 分配, 光 GC 就够呛; 批量读是连续 I/O, 快一个量级。 const batch = 512 buf := make([]byte, batch*PGSZ) for base := 1; base <= numPages; base += batch { n := batch if base+n-1 > numPages { n = numPages - base + 1 } if _, rerr := f.ReadAt(buf[:n*PGSZ], pgOff(uint32(base))); rerr != nil && rerr != io.EOF { break } for k := 0; k < n; k++ { pid := uint32(base + k) off := k * PGSZ us := float64(le32(buf[off+16:off+20])) / float64(PGSZ) * 100 if us > 100 { us = 100 } idx.usg[pid] = uint8(us + 0.5) raw := int(buf[off]) switch { case pid == 1: raw = PG_RESERVED // 引擎预留页, 从不分配 case pid >= firstFree: raw = PG_NEVER // 从未分配过的尾页 case freeSet[pid]: raw = PG_HEAD // 空闲链上的页 case raw == PG_HEAD: // 类型字节=0 却不在空闲链上: 已下发区间内的零页 —— 孤儿/泄漏页。 raw = PG_ORPHAN } pt, tn := pmKind(raw) idx.typ[pid] = int8(pt) idx.counts[tn]++ } } return idx } // pagemapCached 取页索引 (首次调用时同步构建, 之后命中缓存)。 // 缓存随切库代数 (auditGen) 失效, 避免旧库的结果污染新库。 func pagemapCached() *pageIndex { cacheMu.Lock() gen := auditGen cacheMu.Unlock() pmMu.Lock() if pmIdx != nil && pmGen == gen { idx := pmIdx pmMu.Unlock() return idx } pmMu.Unlock() // 构建很慢 (要读满库), 串行化: 并发的调用者排队, 不复算。 pmBuildMu.Lock() defer pmBuildMu.Unlock() pmMu.Lock() if pmIdx != nil && pmGen == gen { idx := pmIdx pmMu.Unlock() return idx } pmMu.Unlock() idx := buildPageIndex() pmMu.Lock() pmIdx, pmGen = idx, gen pmMu.Unlock() return idx } // startPagemapAsync 后台预热页索引 (幂等)。 // 不依赖审计, 所以和审计并行跑; 打开库后几秒页图就能用。 func startPagemapAsync() { go pagemapCached() } // pagemapH 分块返回页映射。 // // /api/pagemap -> 第一块 (含 total/counts 等元信息) // /api/pagemap?meta=1 -> 只要元信息 (total/counts), 不返回页数据 // /api/pagemap?start=1&count=20000 -> 指定区间 // // 类型与使用率都编成 base64 字节串: 每页各 1 字节 (类型 +8 平移到无符号域), // 比 JSON 数字数组小一个量级。 func pagemapH(w http.ResponseWriter, r *http.Request) { if dbF == "" { jw(w, map[string]any{"e": "no db"}) return } q := r.URL.Query() idx := pagemapCached() meta := map[string]any{ "total": idx.total, "counts": idx.counts, "allocated": idx.allocated, "neverUsed": idx.neverUsed, "firstFree": idx.firstFree, "chunk": pmChunkDefault, } if q.Get("meta") != "" { jw(w, meta) return } start, _ := strconv.Atoi(q.Get("start")) if start < 1 { start = 1 } count, _ := strconv.Atoi(q.Get("count")) if count <= 0 || count > pmChunkMax { count = pmChunkDefault } if idx.total <= 0 { meta["start"], meta["count"], meta["done"] = 0, 0, true meta["t"], meta["u"] = "", "" jw(w, meta) return } if start > idx.total { start = idx.total + 1 } end := start + count if end > idx.total+1 { end = idx.total + 1 } tb := make([]byte, 0, end-start) ub := make([]byte, 0, end-start) for pid := start; pid < end; pid++ { tb = append(tb, byte(idx.typ[pid]+8)) ub = append(ub, idx.usg[pid]) } meta["start"], meta["count"], meta["done"] = start, end-start, end >= idx.total+1 meta["t"] = base64.StdEncoding.EncodeToString(tb) meta["u"] = base64.StdEncoding.EncodeToString(ub) jw(w, meta) }