| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- 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 万个 <div> 拼成一个巨型字符串塞进 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",
- }
- // 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, "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)
- }
|