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)
}