| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685 |
- package main
- import (
- "crypto/sha1"
- "encoding/binary"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "math"
- "math/big"
- "net/http"
- _ "net/http/pprof" // /debug/pprof/* 挂在 DefaultServeMux 上, 供排查内存/CPU
- "os"
- "path/filepath"
- "runtime"
- "runtime/debug"
- "strconv"
- "strings"
- "sync"
- "time"
- "unsafe"
- // analyzer 的 go.mod 有 replace TsDB2 => ../, 可以直接读 core 的类型表。
- // 列型的宽度/名字改为投影自 typeRegistry, 见 fSz / fName 的注释。
- "git.ycl.pub/yl32940/TsDB2/DB/core"
- "git.ycl.pub/yl32940/TsDB2/DB/core/types"
- )
- const (
- PGSZ = 16384
- HDRSZ = 4096
- PHDR = 20 // PageHdr actual size (with alignment)
- DP_HDR = 48 // DataPage header (DB/core/page/datapage.go DataPageHdrSize):
- // PageHdr(20) + Count(4) + Min(8) + Max(8) + LiveCnt(4) + Dead(4) = 48
- // v10 在 Count/Min/Max 之后新增了 LiveCnt/Dead 用于 O(1) COUNT 与
- // 失效占比判断; 漏掉这 8B 会让 eachRow 从 LiveCnt/Dead 上读出
- // 假 row header, pkID 全部错位, 进而把整库二级索引条目误判为悬空。
- IDX_HDR = 32 // PHDR(20) + pad(1) + IndexType(1) + EntryLen(2) + Count(4) + IsLeaf(1) + pad(3)
- // 下面两个尺寸与 TI_* / IM_* 偏移与 DB/core 的 IndexMeta/TableInfo 布局一一对应。
- // TableNameSize = 48 且 IndexNameSize = 64 时: IndexMeta 100 字节, TableInfo 104 字节。
- // 这里的每一个常量都由 layout_assert.go 的编译期断言钉在 DB/core 的真实结构体上,
- // 改了 DB/core 布局却漏改这里会直接编译失败 (而不是静默按错位读盘)。
- META_SZ = 100 // IndexMeta entry size (with Go alignment padding)
- TINFO_SZ = 104 // TableInfo struct size (with alignment)
- PG_HEAD = 0
- PG_TABLE = 1
- PG_DATA = 2
- PG_HEAP = 3
- PG_IMETA = 4
- PG_IDATA = 5
- PG_OVER = 6 // DataOverflowPage: 数据页溢出页 (已废弃)
- PG_WAL = 7 // WALIndex: WAL 日志页
- PG_SEXT = 8 // SchemaExtIndex: 全库共享的表结构扩展页
- // 分析器自造的哨兵
- PG_NEVER = -1 // 页号 >= NextFreePage, 从未被分配过
- PG_RESERVED = -2 // 页 1: 引擎预留, 从不分配 (header_page.go newHeader)
- PG_ORPHAN = -3 // 已下发区间内、类型字节=0 却不在空闲链上 (被遗漏的空闲页/泄漏页)
- PG_OTHER = -4 // 未知类型字节
- // PageHdr 字段偏移 (共 20 字节)
- PH_REF = 2 // RefCount uint16 (仅堆页维护: 指向本页的活引用数)
- PH_PREV = 8 // PrePage uint32
- PH_NEXT = 12 // NextPage uint32
- PH_USE = 16 // UseSize uint32
- // 表页在 PageHdr 之后为共享扩展页链头 (SchemaExtPageID) 统一预留 4 字节,
- // 因此每条表记录的起始偏移是 PHDR + SEXT_HDR (所有表页一致)。
- SEXT_HDR = 4
- REC_OFF = PHDR + SEXT_HDR
- // SchemaFragHdr: NextPage(4B BE) + NextOff(2B BE) + DataLen(2B BE)
- FRAG_HDR = 8
- // TableInfo 字段偏移 (共 104 字节, TableNameSize=48)
- TI_FLAG = 48 // Flag byte (0x80 = 已删除/空洞)
- TI_HPCNT = 52 // HeapPageCount uint32 (复用 TotalRows 前的对齐填充)
- TI_ROWS = 56 // TotalRows uint64
- TI_FHEAP = 64 // FirstHeap uint32
- TI_FDATA = 72 // FirstData uint32
- TI_ROWSZ = 80 // RowSize uint16
- TI_ALLOC = 82 // FieldAllocateSize uint16
- TI_FIDX = 88 // FirstIndexID uint32
- TI_EXTPG = 96 // ExtPage uint32 (schema 续块所在扩展页)
- TI_EXTOFF = 100 // ExtOff uint16 (首个续块在页内的偏移)
- // IndexMeta 字段偏移 (共 100 字节, IndexNameSize=64)
- //
- // 这些偏移全都随 IndexNameSize 整体平移 —— 索引名数组一变长, 其后每个字段的位置
- // 都跟着变。名字长度单独列成 IM_NAMSZ, slice 时就不必再写 64 这个魔法数字。
- IM_NAMSZ = 64 // IndexName [IndexNameSize]byte 的字节数 (索引名: im[0:64])
- IM_TYPE = 64 // IndexType byte
- IM_FCNT = 65 // FieldCnt byte
- IM_FLDS = 66 // FieldIDs [16]byte
- IM_EP = 84 // EntryPage uint32 (跳过 2 字节对齐填充)
- IM_FLAG = 96 // Flag byte
- // 索引 Flag 的位 (对应 core 的 IndexFlag*: bit7 软删除 / bit0 唯一索引)。
- // 取值由 layout_assert.go 拿 core 的常量编译期钉住 —— 这里手写位掩码是为了
- // 不依赖 core 的常量名, 但绝不能和它漂移。
- IM_FLAG_DELETED = 0x80
- IM_FLAG_UNIQUE = 0x01
- // 列 (FieldInfo) Flag 的位, 对应 core/types 的 Flag* 常量:
- // bit7 = 已删除 / bit6 = 可为 NULL / bit5 = 唯一
- // bit4..bit0 = 列级类型参数 (按类型解释: decimal 是 scale, 字符串列是宽度码)
- // 掩码名沿用 FLAG_SCALE (core 侧叫 FlagScaleMask), 两个用途共用同一格。
- FLAG_DELETED = 0x80
- FLAG_NULLABLE = 0x40
- FLAG_UNIQUE = 0x20
- FLAG_SCALE = 0x1f
- )
- // ===== Data Structures =====
- type TInfo struct {
- Name string `json:"name"`
- RowN uint64 `json:"rowN"`
- FSz uint16 `json:"fsz"`
- DP0 uint32 `json:"dp0"`
- IP0 uint32 `json:"ip0"`
- HP0 uint32 `json:"hp0"`
- HPC uint32 `json:"hpc"` // HeapPageCount: 声明式堆链长度
- ActiveVer byte `json:"activeVer"`
- MasterFields []MFInfo `json:"masterFields"`
- Versions []VInfo `json:"versions"`
- Idxs []IInfo `json:"idxs"`
- }
- type MFInfo struct {
- Name string `json:"name"`
- Type byte `json:"type"`
- TypeName string `json:"typeName"`
- // Flag 列级标志字节, 随 schema 落盘 (对应 types.FieldInfo.Flag):
- //
- // bit7 bit6 bit5 | bit4 ........ bit0
- // 删除 可为 NULL 唯一 | 列级类型参数 (0..31)
- //
- // 低 5 位按类型解释 (如 decimal:2 → 2, maxlen 声明出来的字符串列 → 宽度码);
- // 删除标记不再占用 Typ 的最高位。
- Flag byte `json:"flag"`
- // Param 列级类型参数 (Flag 低 5 位): decimal 是 scale, 字符串家族是行内宽度码,
- // 其余类型为 0。
- Param byte `json:"param"`
- // Nullable / Unique 从 Flag 拆出来的状态位。
- //
- // 位掩码只在这一个地方拆一次, 前端 / 检查逻辑都用这两个布尔 —— 每个消费方
- // 各自去 &0x40 / &0x20, 迟早会有人写错位, 而"列可不可空"读错的后果是悄悄
- // 把 NULL 当成 0 来解释。
- Nullable bool `json:"nullable"`
- Unique bool `json:"unique"`
- Sz int `json:"sz"`
- // Label / Tip 类型徽章的展示文本与悬浮说明, 由 fLabel / fTip 在【后端】算好。
- //
- // 字符串列的 label 会显式带出行内宽度 (string(1024B)): 类型档 (string) 与真实
- // 宽度矛盾时, 列表里能一眼看出这列到底占多大, 不必往右看 Size 列。
- // 放在后端算与 nullable/unique 同一个理由: Flag 低 5 位是【按类型重载】的一格
- // (decimal 是 scale, 字符串家族是宽度码), 前端若能拿到它, 迟早有人当字节数显示 ——
- // 而"宽度码 20"对应的那列是 1024 字节, 差 50 倍。
- Label string `json:"label"`
- Tip string `json:"tip"`
- Dropped bool `json:"dropped"`
- }
- type VInfo struct {
- Ver byte `json:"ver"`
- Fields []string `json:"fields"`
- // Indices 本版字段在 masterFields 里的下标 (顺序 = 行内字段顺序)。
- //
- // 对外必须是 []int 而不是盘上原样的 []byte: 盘上每版字段数是 1 字节, 解析时直接
- // copy 最省事, 但 []byte 过 JSON 会变成 base64 字符串 —— 前端 v.indices[i] 拿到的是
- // 第 i 个字符, t.masterFields['A'] 恒为 undefined, "这一版每列多宽"整块静默消失。
- // 宽化在解析层一次做完, 前端拿到的就是能直接当下标用的数字。
- Indices []int `json:"indices"`
- }
- type FInfo struct {
- Name string `json:"name"`
- Type byte `json:"type"`
- Sz int `json:"sz"`
- }
- type IInfo struct {
- Name string `json:"name"`
- Type byte `json:"type"`
- TS string `json:"ts"`
- EP uint32 `json:"ep"`
- Flds []int `json:"flds"`
- // Uniq 唯一索引 (IndexMeta.Flag 的 bit0)。索引的 Flag 字节里只有两位:
- //
- // bit7 bit6 bit0
- // 已删除 (未用) 唯一索引
- //
- // 唯一位不参与一致性校验 (它只约束写入路径, 不影响盘上结构), 但漏掉它会让
- // 分析器把唯一索引和普通索引用同一种样子画出来 —— 从外部看, 一张表的约束
- // 就完全不可见了。
- Uniq bool `json:"uniq"`
- }
- type DP struct {
- ID uint32 `json:"id"`
- Nx uint32 `json:"nx"`
- Pv uint32 `json:"pv"`
- Cn uint32 `json:"cn"`
- Mx uint64 `json:"mx"`
- Mn uint64 `json:"mn"`
- Us float64 `json:"us"`
- Rs []DR `json:"rs"`
- }
- type DR struct {
- I int `json:"i"`
- PKID uint64 `json:"pk"`
- SchemaV byte `json:"sv"`
- Hex string `json:"h"`
- Decoded string `json:"decoded"`
- }
- type IP struct {
- ID uint32 `json:"id"`
- Nx uint32 `json:"nx"`
- Pv uint32 `json:"pv"`
- Lf bool `json:"lf"`
- Cn uint32 `json:"cn"`
- Us float64 `json:"us"`
- Ents []IE `json:"ents"`
- IdxType byte `json:"it"`
- }
- type IE struct {
- K string `json:"k"`
- P uint32 `json:"p"`
- DP uint64 `json:"dp"`
- }
- type HdrInfo struct {
- Magic string `json:"magic"`
- Version uint32 `json:"ver"`
- PageSize uint32 `json:"pgsz"`
- NextFree uint32 `json:"nf"`
- TablePage uint32 `json:"tp"`
- TableCount uint32 `json:"tc"`
- CreateTime string `json:"ct"`
- ModifyTime string `json:"mt"`
- FreeStart uint32 `json:"fs"`
- FreeEnd uint32 `json:"fe"`
- FreeCount uint32 `json:"fc"`
- GlobalFlags uint64 `json:"gf"`
- WALFirst uint32 `json:"wal0"`
- WALLast uint32 `json:"wal1"`
- TxnID uint64 `json:"txn"`
- Comment string `json:"cm"`
- HeaderCrc uint32 `json:"crc"` // 头页声明的整页 XXH32 (旧库为 0)
- CrcVerdict string `json:"cv"` // 校验结论: OK / CRC 不符 / 跳过
- CrcDetail string `json:"cd"` // 校验说明
- // 实算值 (与上面的声明值互校)
- FreeWalked int `json:"fw"` // 实走空闲链长度
- Pages int `json:"pg"` // 数据页总数
- Allocated int `json:"ap"` // 已分配页数
- NeverUsed int `json:"nu"` // 从未分配的尾页
- FreeVerdict string `json:"fv"` // 空闲链自检结论
- FreeDetail string `json:"fd"` // 自检说明
- }
- type HeapChain struct {
- Pages []HeapPageInfo `json:"pages"`
- Total int `json:"total"`
- }
- type HeapPageInfo struct {
- ID uint32 `json:"id"`
- Next uint32 `json:"nx"`
- Prev uint32 `json:"pv"`
- Use uint32 `json:"use"`
- Us float64 `json:"us"`
- Ref uint16 `json:"ref"` // 页头 RefCount: 指向本页的活引用数 (仅堆页有意义)
- }
- var (
- dbF string
- fp string
- hdr [HDRSZ]byte
- tabs []TInfo
- mu sync.RWMutex
- )
- func main() {
- log.SetFlags(log.LstdFlags | log.Lshortfile)
- fmt.Println("╔══════════════════════════════════════════════╗")
- fmt.Println("║ TsDB2 Analyzer v3.0 (Schema Aware) ║")
- fmt.Println("║ http://localhost:8081 ║")
- fmt.Println("╚══════════════════════════════════════════════╝")
- http.HandleFunc("/", homeH)
- http.HandleFunc("/api/list-dir", ldH)
- http.HandleFunc("/api/open-db", openH)
- http.HandleFunc("/api/stats", stH)
- http.HandleFunc("/api/tables", taH)
- http.HandleFunc("/api/table/", tdH)
- http.HandleFunc("/api/data-page", dpH)
- http.HandleFunc("/api/index-page", ipH)
- http.HandleFunc("/api/btree", btH)
- http.HandleFunc("/api/header", hdrH)
- http.HandleFunc("/api/heap-chain", heapH)
- http.HandleFunc("/api/free-pages", freeH)
- http.HandleFunc("/api/pages", pagesH)
- http.HandleFunc("/api/pagemap", pagemapH)
- http.HandleFunc("/api/find-pk", findPkH)
- http.HandleFunc("/api/audit", auditH)
- http.HandleFunc("/api/mem", memH)
- http.HandleFunc("/api/audit/issues", auditIssuesH)
- http.HandleFunc("/api/audit/unacc", auditUnaccH)
- http.HandleFunc("/api/heap-chains", heapChainsH)
- http.HandleFunc("/api/page-info", pageInfoH)
- // 静态资源 (index.html 引用的 js/css/字体等) 一律走 embed, 不再暴露工作目录。
- http.Handle("/s/", http.StripPrefix("/s/", http.FileServer(http.FS(webRoot()))))
- log.Fatal(http.ListenAndServe(":8081", countInflight(http.DefaultServeMux)))
- }
- // countInflight 把每个请求登记为"在飞读任务": 请求期间可能持有指向映射区的
- // 页切片, 换库时不能把映射解掉 (见 audit.go 的 gcRetired)。
- func countInflight(h http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- enterRead()
- defer exitRead()
- h.ServeHTTP(w, r)
- })
- }
- // ===== Low-level page reading =====
- // rp 读一页 —— 统一走 audit.go 的共享句柄 (全库审计要读上千页, 不能每页都 open)。
- func rp(pid uint32) ([]byte, error) { return readPage(pid) }
- // rHdr 把 4096 字节的 HeaderPage 读进全局 hdr。
- func rHdr() {
- f, e := dbFile()
- if e != nil {
- return
- }
- f.ReadAt(hdr[:], 0)
- }
- // ===== Parse all tables from TablePage chain =====
- func pTabs() []TInfo {
- var R []TInfo
- tp := binary.LittleEndian.Uint32(hdr[48:52]) // TablePage (v11 起偏移 48)
- if tp == 0 {
- return R
- }
- for pid := tp; pid != 0; {
- b, e := rp(pid)
- if e != nil {
- break
- }
- us := binary.LittleEndian.Uint32(b[PH_USE : PH_USE+4])
- if us > PGSZ {
- us = PGSZ
- }
- // 所有表页都在 PageHdr 之后统一预留 SEXT_HDR 字节, 记录从 REC_OFF 开始
- off := uint32(REC_OFF)
- for off+TINFO_SZ <= us && off+TINFO_SZ <= PGSZ {
- tb := b[off : off+TINFO_SZ]
- alloc := uint32(binary.LittleEndian.Uint16(tb[TI_ALLOC : TI_ALLOC+2]))
- if tb[TI_FLAG]&0x80 != 0 {
- // Drop 留下的空洞 (墓碑): 按记录自身长度跳过
- off += TINFO_SZ + alloc
- continue
- }
- nm := strings.TrimRight(string(tb[:48]), "\x00") // 表名: [0, TableNameSize)
- rn := binary.LittleEndian.Uint64(tb[TI_ROWS : TI_ROWS+8])
- hpc := binary.LittleEndian.Uint32(tb[TI_HPCNT : TI_HPCNT+4])
- // 重建完整 schema: 内联块 (alloc 字节) + 共享扩展页续块链
- schema := readSchemaBytes(b, off, alloc, tb)
- // Parse new schema format
- activeVer, masterFields, versions := parseSchemaFields(schema)
- // Parse indexes
- var is []IInfo
- iip := binary.LittleEndian.Uint32(tb[TI_FIDX : TI_FIDX+4])
- for ip := iip; ip != 0; {
- ib, e := rp(ip)
- if e != nil {
- break
- }
- if ib[0] != PG_IMETA {
- break
- }
- iu := binary.LittleEndian.Uint32(ib[PH_USE : PH_USE+4])
- if iu > PGSZ {
- iu = PGSZ
- }
- io := uint32(PHDR)
- for io+META_SZ <= iu && io+META_SZ <= PGSZ {
- im := ib[io : io+META_SZ]
- // IndexMeta struct (Name[64] + Type + FieldCnt + FieldIDs[16] + pad(2)
- // + EntryPage + Prev + Next + Flag + pad(3) = 100): 各字段偏移见文件头的 IM_*
- // 常量, 由 layout_assert.go 的编译期断言钉在 DB/core 的真实结构体上。
- if im[IM_FLAG]&IM_FLAG_DELETED == 0 {
- in := strings.TrimRight(string(im[:IM_NAMSZ]), "\x00") // IndexName: [0, IndexNameSize)
- it := im[IM_TYPE] // IndexType
- ic := int(im[IM_FCNT]) // FieldCnt
- ep := binary.LittleEndian.Uint32(im[IM_EP : IM_EP+4]) // EntryPage (跳过 2B 对齐填充)
- uq := im[IM_FLAG]&IM_FLAG_UNIQUE != 0 // 唯一索引
- // 验证 EntryPage 指向有效索引页 (防止已删除表的孤立条目)
- if ep != 0 {
- pb, pe := rp(ep)
- if pe != nil || pb[0] != PG_IDATA {
- ep = 0 // 无效页, 跳过
- }
- }
- if ep != 0 {
- var fds []int
- for j := 0; j < ic; j++ {
- fds = append(fds, int(im[IM_FLDS+j])) // FieldIDs
- }
- is = append(is, IInfo{Name: in, Type: it, TS: tNm(it), EP: ep, Flds: fds, Uniq: uq})
- }
- }
- io += META_SZ
- }
- ip = binary.LittleEndian.Uint32(ib[PH_NEXT : PH_NEXT+4])
- }
- R = append(R, TInfo{
- Name: nm, RowN: rn,
- FSz: binary.LittleEndian.Uint16(tb[TI_ROWSZ : TI_ROWSZ+2]),
- DP0: binary.LittleEndian.Uint32(tb[TI_FDATA : TI_FDATA+4]),
- IP0: binary.LittleEndian.Uint32(tb[TI_FIDX : TI_FIDX+4]),
- HP0: binary.LittleEndian.Uint32(tb[TI_FHEAP : TI_FHEAP+4]),
- HPC: hpc,
- ActiveVer: activeVer, MasterFields: masterFields, Versions: versions, Idxs: is,
- })
- off += TINFO_SZ + alloc
- }
- nx := binary.LittleEndian.Uint32(b[PH_NEXT : PH_NEXT+4])
- if nx == 0 || nx == pid {
- break
- }
- pid = nx
- }
- return R
- }
- // readSchemaBytes 重建一张表的完整 schema 字节:
- // 表页内联块 (alloc 字节) + 共享扩展页里的续块链 [SchemaFragHdr 8B][payload]
- func readSchemaBytes(pageBuf []byte, recOff, alloc uint32, tb []byte) []byte {
- schema := make([]byte, 0, alloc)
- start := recOff + TINFO_SZ
- if end := start + alloc; end <= uint32(len(pageBuf)) {
- schema = append(schema, pageBuf[start:end]...)
- } else if start < uint32(len(pageBuf)) {
- schema = append(schema, pageBuf[start:]...)
- }
- extPage := binary.LittleEndian.Uint32(tb[TI_EXTPG : TI_EXTPG+4])
- extOff := uint32(binary.LittleEndian.Uint16(tb[TI_EXTOFF : TI_EXTOFF+2]))
- for pid, off := extPage, extOff; pid != 0; {
- fb, e := rp(pid)
- if e != nil || int(off)+FRAG_HDR > len(fb) {
- break
- }
- nextPage := binary.BigEndian.Uint32(fb[off : off+4])
- nextOff := binary.BigEndian.Uint16(fb[off+4 : off+6])
- dataLen := int(binary.BigEndian.Uint16(fb[off+6 : off+8]))
- s := int(off) + FRAG_HDR
- if s+dataLen > len(fb) {
- break
- }
- schema = append(schema, fb[s:s+dataLen]...)
- pid, off = nextPage, uint32(nextOff)
- }
- return schema
- }
- // parseSchemaFields 解析新版 schema 字段定义链
- //
- // 格式 (与 DB/core 的 serializeSchema 逐字节对应):
- //
- // [activeVer 1B][masterCount 1B][typ 1B][flag 1B][nameLen 1B][name]×masterCount
- // [verCount 1B][ver 1B][indexCount 1B][indices]×verCount
- //
- // flag 是**列级标志字节**, 紧跟在 typ 之后 (对应 types.FieldInfo.Flag):
- //
- // bit7 bit6 bit5 | bit4 ........ bit0
- // 删除 可为 NULL 唯一 | 列级类型参数 (0..31)
- //
- // 字节位置与原来的 param 完全一致, 只是含义被扩展了: 低 5 位【按类型解释】——
- // decimal 是 scale (例如 `12 02 06 "Amount"` 就是 decimal(2)), 字符串家族是行内宽度码
- // (maxlen 声明出来的列靠它决定宽度)。其余类型低 5 位为 0, 但字节一定在。
- //
- // 所以下面算 Sz 时必须把 flag 一起传进去 (fSzWithParam): 只看 typ 会给宽度码列算成
- // 类型默认档 256, 而它是 2048 就整行错位 —— 与本函数注释开头说的那次事故同一类。
- //
- // 【这里漏掉这个字节曾造成整表读错】: 原实现按 `[typ][nameLen][name]` 走位, 于是把
- // 它当成了 nameLen —— 第一个字段名读成空串, 之后 masterFields / versions 全部错位。
- // 症状很误导人: 表显示成"只有 1 个字段且无名"、版本号是乱数、数据页一行都解不出, 而
- // 页头那些不依赖 schema 的字段 (TotalRows / RowSize / 页号) 却完全正确, 于是很难往
- // "schema 解析"上想。TestParseSchemaAgainstCore 用 core 造的**真库**来钉这件事:
- // 它不重述格式, 而是要求两侧对同一份盘上字节的解释一致。
- func parseSchemaFields(b []byte) (byte, []MFInfo, []VInfo) {
- if len(b) < 2 {
- return 0, nil, nil
- }
- pos := 0
- activeVer := b[pos]
- pos++
- masterCount := int(b[pos])
- pos++
- var masterFields []MFInfo
- for i := 0; i < masterCount && pos+3 <= len(b); i++ {
- typ := b[pos]
- flag := b[pos+1]
- nameLen := int(b[pos+2])
- pos += 3
- if pos+nameLen > len(b) {
- break
- }
- name := strings.TrimRight(string(b[pos:pos+nameLen]), "\x00")
- pos += nameLen
- // typ 是纯类型值 (删除标记不再占最高位), 状态位全在 flag 里。
- // param / sz 各算一次并复用: 宽度是"行内按多少字节付满"的唯一依据,
- // 下面三处 (Sz / Label / Tip) 必须拿到同一个值, 分头算迟早会漂移。
- param := flag & FLAG_SCALE
- sz := fSzWithParam(typ, param)
- masterFields = append(masterFields, MFInfo{
- Name: name, Type: typ, TypeName: fName(typ),
- Flag: flag, Sz: sz, Param: param,
- Label: fLabel(typ, param, sz),
- Tip: fTip(typ, param, sz),
- Dropped: flag&FLAG_DELETED != 0,
- Nullable: flag&FLAG_NULLABLE != 0,
- Unique: flag&FLAG_UNIQUE != 0,
- })
- }
- var versions []VInfo
- if pos < len(b) {
- verCount := int(b[pos])
- pos++
- for i := 0; i < verCount && pos < len(b); i++ {
- verNo := b[pos]
- pos++
- if pos >= len(b) {
- break
- }
- fieldCount := int(b[pos])
- pos++
- // 下标逐个宽化成 int (见 VInfo.Indices): 盘上 1 字节/个, 但对外要数字数组。
- indices := make([]int, fieldCount)
- if pos+fieldCount <= len(b) {
- for i, raw := range b[pos : pos+fieldCount] {
- indices[i] = int(raw)
- }
- pos += fieldCount
- }
- // Build field name list for this version
- var fieldNames []string
- for _, idx := range indices {
- if idx < len(masterFields) {
- mf := masterFields[idx]
- n := mf.Name
- if mf.Dropped {
- n += " (dropped)"
- }
- fieldNames = append(fieldNames, n)
- } else {
- fieldNames = append(fieldNames, "?")
- }
- }
- versions = append(versions, VInfo{Ver: verNo, Fields: fieldNames, Indices: indices})
- }
- }
- return activeVer, masterFields, versions
- }
- // ===== Field type helpers =====
- // fSz 列在行内的字节宽度。
- //
- // 【为什么是投影而不是自己 switch】这里原本手写了一张 1..15 的表, core 新增 7 个列型
- // (bool/float32/decimal/uuid/year/int128/blob) 时没同步, 于是 fSz 对新类型返回 0 ——
- // 这不是"显示成 ?"那么轻: 行解码靠它推进偏移 (decodeRows 里的 off += sz), 0 会让新类型
- // 字段【之后的所有列】都从同一个错位偏移解码, 整行静默错位; indexcheck 也会因为
- // size <= 0 直接放弃索引键校验。
- //
- // 改成投影到 core 的 typeRegistry 之后, 这类漏同步在结构上不可能再发生: core 那边有
- // allTypes + CheckRegistry 兜底, 这里自动覆盖全部已声明类型。复用 core 的布局事实是
- // 本项目既有做法 (见 layout_assert.go 的编译期断言)。
- //
- // 注意它给的是【类型默认档】宽度; 行内真实宽度还要乘上列级参数 (字符串列的宽度码),
- // 行解码/索引键切分一律用 fSzWithParam。
- func fSz(t byte) int { return int(types.GetSize(types.FieldType(t))) }
- // fSzWithParam 列在行内的真实字节宽度 = types.FieldSize(类型, 列级参数)。
- //
- // param 取列 flag 的低 5 位 (字符串列是宽度码, decimal 是 scale)。少传它就会给
- // "maxlen 声明出来的字符串列"算出类型默认档 256, 而盘上那一列是 1024 字节 ——
- // off += sz 之后整行字段静默错位, 且错得"很像正常数据" (字段都有值, 只是全错位)。
- // 返回 0 表示类型未注册或参数非法: 调用方必须放弃解析该行, 不能当真值用。
- func fSzWithParam(t byte, param byte) int {
- return int(types.FieldSize(types.FieldType(t), param))
- }
- func tNm(t byte) string {
- switch t {
- case 1:
- return "Time"
- case 2:
- return "Primary"
- case 3:
- return "String"
- case 4:
- return "Compose"
- case 5:
- return "BTree"
- case 6:
- return "Hash"
- default:
- return "?"
- }
- }
- // fName 列类型的可读名。同样投影自 core 的 typeRegistry。
- //
- // 名字一律投影自 types 注册表 (uint8/uint16/…/string/dynamic/float64/int8/…/int64),
- // 因此 core 增删类型时这里自动跟随 —— 不需要再维护第二份会漂移的名单。
- func fName(t byte) string {
- if n := types.TypeNameOf(types.FieldType(t)); n != "" {
- return n
- }
- return "?"
- }
- // fLabel 类型徽章的展示文本 (MFInfo.Label)。
- //
- // 【为什么字符串列要改写】定长字符串列的行内宽度不是类型自带的, 而是靠 Flag 低 5 位的
- // 【宽度码】声明 (见 types.FieldSize)。于是同一个 String 类型下会出现"真实行宽 1024 字节"
- // 的列 —— 列表里只写类型名, 看起来就是默认的 256 字节, 与盘上差了 4 倍, 而"字符串大小"
- // 这件事恰恰是看 schema 时最想知道的。所以宽度码不为 0 的字符串列显示成 string(1024B)。
- //
- // 宽度码为 0 (沿用类型默认宽度 256) 时只显示类型名 string。
- //
- // decimal 显示 scale (与改动前一致); 其余类型就是类型名; 未注册类型给 "?"。
- func fLabel(t byte, param byte, sz int) string {
- name := fName(t)
- if name == "?" {
- return unknownFieldValue
- }
- ft := types.FieldType(t)
- switch {
- case ft == types.FieldType_Decimal:
- if param > 0 {
- return fmt.Sprintf("%s(%d)", name, param)
- }
- case types.IsStringType(ft) && param > 0:
- return fmt.Sprintf("string(%dB)", sz)
- }
- return name
- }
- // fTip 类型徽章的悬浮说明 (MFInfo.Tip): 把"类型档 + 列级参数 + 行内宽度"三件事一次讲清。
- //
- // 徽章上放不下这些, 而它们正是排查行宽问题时要对照的东西 —— 尤其是字符串列:
- // 盘上只存宽度码, 声明值本身【没有保留】, 因此这里只能给"向上取档后的宽度",
- // 这一点必须在提示里说破, 否则会有人拿它当声明值对账。
- func fTip(t byte, param byte, sz int) string {
- name := fName(t)
- if name == "?" {
- return fmt.Sprintf("未注册的类型 %d: 行内宽度算不出来, 本行无法解码", t)
- }
- ft := types.FieldType(t)
- switch {
- case types.IsStringType(ft):
- tip := fmt.Sprintf("定长字符串列: 类型档 %s, 行内宽度 %d 字节", name, sz)
- if param > 0 {
- tip += fmt.Sprintf(" (宽度码 %d; 声明容量向上取档后的宽度, 每行按它付满)", param)
- }
- return tip
- case ft == types.FieldType_Decimal:
- return fmt.Sprintf("定点小数: scale=%d, 行内宽度 %d 字节", param, sz)
- case sz <= 0:
- return fmt.Sprintf("类型 %s: 行内宽度算不出来", name)
- }
- return fmt.Sprintf("%s: 行内宽度 %d 字节", name, sz)
- }
- // ===== Read DataPage with schema version awareness =====
- // rDP reads a DataPage (sequential row layout, no directory)
- func rDP(pid uint32) *DP {
- b, e := rp(pid)
- if e != nil {
- return nil
- }
- if b[0] != PG_DATA {
- return nil
- }
- cn := binary.LittleEndian.Uint32(b[PHDR : PHDR+4])
- mn := binary.BigEndian.Uint64(b[PHDR+4 : PHDR+12])
- mx := binary.BigEndian.Uint64(b[PHDR+12 : PHDR+20])
- us := float64(binary.LittleEndian.Uint32(b[16:20])) / float64(PGSZ) * 100
- var rs []DR
- off := DP_HDR
- for i := uint32(0); i < cn && off+4 <= len(b); i++ {
- // 长度字段最高位 (0x80000000) 为软删除标志, 必须屏蔽后再取实际长度
- rawLen := binary.LittleEndian.Uint32(b[off : off+4])
- rl := int(rawLen &^ 0x80000000)
- deleted := rawLen&0x80000000 != 0
- off += 4
- if off+rl > len(b) || rl <= 0 {
- break
- }
- storeRow := b[off : off+rl]
- off += rl
- if deleted {
- continue
- }
- // storeRow = [pkID(8B BE)] + rowData
- var pkID uint64
- var rowData []byte
- if len(storeRow) >= 8 {
- pkID = binary.BigEndian.Uint64(storeRow[:8])
- rowData = storeRow[8:]
- } else {
- rowData = storeRow
- }
- schemaV := byte(0)
- if len(rowData) > 0 {
- schemaV = rowData[0]
- }
- hx := fmt.Sprintf("%x", storeRow[:min(len(storeRow), 64)])
- if len(storeRow) > 64 {
- hx += "..."
- }
- decoded := decodeRowData(rowData, schemaV)
- rs = append(rs, DR{int(i), pkID, schemaV, hx, decoded})
- }
- return &DP{pid, binary.LittleEndian.Uint32(b[PH_NEXT : PH_NEXT+4]), binary.LittleEndian.Uint32(b[PH_PREV : PH_PREV+4]),
- cn, mx, mn, us, rs}
- }
- // decodeRowData 尝试解码行数据 (显示 schemaVer + 各字段值)
- //
- // 行布局 v9: [schemaVer 1B][列0 W0 B][列结束偏移表 2N B][列1..N-1 内容][NULL 位图 ⌈n/8⌉B]
- // (位图长度按该行自己的版本算, n = 该版本 Indices 长度)。两处必须照做, 否则整行错位:
- // - 列位置一律由【行内偏移表】给出: 列 0 恒占 W0 字节 (档位宽度, 不足补零), 偏移表基址
- // = 1+W0, 表项 0 = 变长内容区起点 (= 1+W0+2N), 表项 i(i≥1) = 列 i 结束偏移, 列 i(i≥1)
- // 起点 = 表项 i-1, 位图起点 = 表末项。v9 字符串列按实际长度存储, 再沿用"累加档位宽度"
- // 定位会越算越偏 (表现为整行错列)。
- // - 列 0 恒占 W0 字节也让时序键固定在绝对偏移 9 (读取侧写死的偏移, W0 == 8 时成立)。
- // - 已删除列在行内【仍占位】(DropField 不新增版本, 该列不会从 Indices 消失), 所以判定
- // Dropped 只能跳过"显示", 绝不能影响列定位 —— 偏移表项照样按 i 取。
- func decodeRowData(rowData []byte, schemaV byte) string {
- if len(rowData) < 1 {
- return ""
- }
- parts := []string{fmt.Sprintf("v%d", schemaV)}
- if len(rowData) < 2 {
- return strings.Join(parts, " ")
- }
- for _, t := range tabs {
- sv := findVersion(t, schemaV)
- if sv == nil {
- continue
- }
- rowN := len(sv.Indices)
- if rowN == 0 {
- break
- }
- if sv.Indices[0] >= len(t.MasterFields) || t.MasterFields[sv.Indices[0]].Sz <= 0 {
- break
- }
- tblBase := 1 + t.MasterFields[sv.Indices[0]].Sz // 偏移表基址 == 列 0 结束偏移
- header := tblBase + 2*rowN // 变长内容区起点 == 表项 0
- if header > len(rowData) {
- break
- }
- if int(binary.BigEndian.Uint16(rowData[tblBase:tblBase+2])) != header {
- break // 表项 0 不是变长内容区起点: 布局不可信, 不猜
- }
- bitmapLen := (rowN + 7) / 8
- prev := header
- bad := false
- for i := 0; i < rowN; i++ {
- off := int(binary.BigEndian.Uint16(rowData[tblBase+2*i:tblBase+2*i+2])) & rowVarOffMask
- if off < prev || off > len(rowData) {
- bad = true
- break
- }
- prev = off
- }
- if bad || prev+bitmapLen > len(rowData) {
- break
- }
- bitmap := rowData[prev : prev+bitmapLen]
- for i, idx := range sv.Indices {
- if idx >= len(t.MasterFields) {
- break
- }
- mf := t.MasterFields[idx]
- start := 1
- if i > 0 {
- start = int(binary.BigEndian.Uint16(rowData[tblBase+2*(i-1):tblBase+2*i])) & rowVarOffMask
- }
- end := tblBase // 列 0 结束恒为 1+W0 == 表基址
- if i > 0 {
- end = int(binary.BigEndian.Uint16(rowData[tblBase+2*i:tblBase+2*i+2])) & rowVarOffMask
- }
- if end < start || end > len(rowData) {
- break
- }
- if !mf.Dropped { // 已删除列仍占位, 只是不显示
- if bitmap[i>>3]&(1<<uint(i&7)) != 0 {
- parts = append(parts, mf.Name+"=NULL")
- } else {
- // 行内可能是实际长度的短值 (字符串列), 补零回档位宽度再解, 与引擎同源。
- parts = append(parts, mf.Name+"="+decodeFieldValue(padToWidth(rowData[start:end], mf.Sz), mf.Type))
- }
- }
- }
- return strings.Join(parts, ", ")
- }
- return strings.Join(parts, " ")
- }
- func findVersion(t TInfo, ver byte) *VInfo {
- for i := range t.Versions {
- if t.Versions[i].Ver == ver {
- return &t.Versions[i]
- }
- }
- return nil
- }
- // unknownFieldValue 未同步列型的显式哨兵。
- //
- // 为什么不用"当作定长字符串打印"兜底: 那样输出看着像正常内容, 实际是错的 ——
- // 而 analyzer 最怕的正是这个 (页照常渲染, 数据全错)。用哨兵才能让
- // TestDecodeCoversEveryRegisteredType 当场发现"core 加了新类型而这里没跟"。
- const unknownFieldValue = "?"
- // decodeFieldValue 把行内字节解成可读文本。约定: len(buf) == 该列的 fSzWithParam(typ, param)。
- //
- // 与 DB/core 的 DecodeValue 是【两份独立实现】, 这是刻意的: analyzer 是"第二读取器",
- // 存在的意义就是独立验一遍 core 读出来的东西 —— 若共用同一份解码代码, core 解错时
- // analyzer 会跟着一起错, 校验价值就没了。所以这里只借用"布局事实" (宽度 fSz / 名字
- // fName, 投影自 core 的 typeRegistry), 值语义自己实现一遍。
- //
- // 每个已注册列型都必须有明确分支; 落到 unknownFieldValue 说明漏同步了。
- func decodeFieldValue(buf []byte, typ byte) string {
- // 一律用 core 导出的类型常量, 不再写字面量序号 —— 否则 core 一旦重排类型值,
- // 这里的 case 会静默错位 (把 1 字节当 2 字节解码, 整段乱掉)。判据同源于 types 注册表。
- switch types.FieldType(typ) {
- // ===== 1 字节 =====
- case types.FieldType_Bool:
- // 盘上只允许 0x00 / 0x01 (写入侧已规范化, 不会存下 2)
- if buf[0] != 0 {
- return "true"
- }
- return "false"
- case types.FieldType_Uint8:
- return fmt.Sprintf("%d", buf[0])
- case types.FieldType_Int8:
- // 有符号列盘上是保序编码 (符号位翻转), 读回来要先翻回去
- return fmt.Sprintf("%d", int8(buf[0]^0x80))
- // ===== 2 字节 =====
- case types.FieldType_Uint16:
- return fmt.Sprintf("%d", binary.BigEndian.Uint16(buf))
- case types.FieldType_Int16:
- return fmt.Sprintf("%d", int16(binary.BigEndian.Uint16(buf)^0x8000))
- case types.FieldType_Year:
- // 无符号大端 uint16 (无保序编码; 解码载体是 int16)
- return fmt.Sprintf("%d", binary.BigEndian.Uint16(buf))
- // ===== 4 字节 =====
- case types.FieldType_Uint32:
- return fmt.Sprintf("%d", binary.BigEndian.Uint32(buf))
- case types.FieldType_Int32:
- return fmt.Sprintf("%d", int32(binary.BigEndian.Uint32(buf)^0x80000000))
- case types.FieldType_Float32:
- // 保序编码 (非负翻符号位, 负数整体取反; 见 types.Float32ToSortKey)
- u := binary.BigEndian.Uint32(buf)
- if u&(1<<31) != 0 {
- u ^= 1 << 31
- } else {
- u = ^u
- }
- return strconv.FormatFloat(float64(math.Float32frombits(u)), 'g', -1, 32)
- // ===== 8 字节 =====
- case types.FieldType_Uint64:
- return fmt.Sprintf("%d", binary.BigEndian.Uint64(buf))
- case types.FieldType_DateTime:
- // 行内固定 8 字节 BE uint64, 存 Unix 毫秒时间戳 (types.go FieldType_DateTime)
- return fmt.Sprintf("%d", binary.BigEndian.Uint64(buf))
- case types.FieldType_Int64:
- return fmt.Sprintf("%d", int64(binary.BigEndian.Uint64(buf)^0x8000000000000000))
- case types.FieldType_Float64:
- // 保序编码: 非负翻符号位, 负数整体取反 (见 types.Float64ToSortKey)
- u := binary.BigEndian.Uint64(buf)
- if u&(1<<63) != 0 {
- u ^= 1 << 63
- } else {
- u = ^u
- }
- return strconv.FormatFloat(math.Float64frombits(u), 'g', -1, 64)
- // ===== 16 字节 =====
- case types.FieldType_Int128:
- return decodeInt128(buf).String()
- case types.FieldType_UUID:
- // 16 字节原样二进制, 显示成标准 8-4-4-4-12
- h := fmt.Sprintf("%x", buf[:16])
- return h[:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:]
- // ===== 18 字节: 定点小数 [16B 系数保序编码][2B scale 大端], 值 = 系数 / 10^scale =====
- case types.FieldType_Decimal:
- return decimalText(decodeInt128(buf), int(binary.BigEndian.Uint16(buf[16:])))
- // ===== 定长字符串 (宽度由 Flag 低 5 位宽度码决定, 与这里无关) =====
- case types.FieldType_String:
- return bytesFieldText(buf)
- // ===== 动态列: 行内只有 6 字节堆引用 [PageID uint32 大端][Offset uint16 大端] =====
- //
- // 以前动态落到 default 分支, 把页号和偏移当字符串打印出来 —— 真正想看的
- // "内容在哪一页" 反而看不见。
- case types.FieldType_Dynamicbyte:
- pid := binary.BigEndian.Uint32(buf)
- if pid == 0 {
- return "(empty)" // PageID == 0 表示空值
- }
- return fmt.Sprintf("(heap page=%d off=%d)", pid, binary.BigEndian.Uint16(buf[4:]))
- // ===== blob 列 (v10): 行内 8 字节链引用 [TotalLen uint32 大端][FirstPageID uint32 大端] =====
- //
- // 与动态列长得像但读法完全不同: 内容不在堆页上的某一块, 而在一条可跨页的链上,
- // 所以这里报"链首页 + 内容长度"; 顺着 page=%d 用 tbl.BlobRead(ref) 能整块读出来。
- case types.FieldType_Blob:
- total := binary.BigEndian.Uint32(buf)
- pid := binary.BigEndian.Uint32(buf[4:])
- if total == 0 && pid == 0 {
- return "(empty)" // (0, 0) 表示空 blob
- }
- return fmt.Sprintf("(blob page=%d len=%d)", pid, total)
- default:
- return unknownFieldValue
- }
- }
- // bytesFieldText 定长字节串的文本形式: 去掉尾部补零, 全空则显示 (empty)。
- func bytesFieldText(buf []byte) string {
- s := strings.TrimRight(string(buf), "\x00")
- if len(s) == 0 {
- return "(empty)"
- }
- return s
- }
- // decodeInt128 还原 core 的 int128ToSortKeyBytes: [16B 补码大端, 最高位已翻转]。
- //
- // 判符号必须在【翻回来之前】看: 盘上最高位为 0 代表原值是负数 (编码时对最高位异或了
- // 0x80); 顺序判反会把全部非负数读成负数。
- func decodeInt128(b []byte) *big.Int {
- var raw [16]byte
- copy(raw[:], b)
- neg := raw[0]&0x80 == 0
- raw[0] ^= 0x80
- v := new(big.Int).SetBytes(raw[:])
- if neg {
- v.Sub(v, new(big.Int).Lsh(big.NewInt(1), 128))
- }
- return v
- }
- // decimalText 把 [系数, scale] 渲染成小数字面量 (值 = 系数 / 10^scale)。
- //
- // 必须按 scale 补足位数: 系数 150 + scale 2 是 "1.50", 而系数 5 + scale 2 是 "0.05"
- // —— 少了补零就会读成 "5", 差 100 倍。
- func decimalText(coef *big.Int, scale int) string {
- s := coef.String()
- if scale <= 0 {
- return s
- }
- sign := ""
- if strings.HasPrefix(s, "-") {
- sign, s = "-", s[1:]
- }
- if len(s) <= scale {
- s = strings.Repeat("0", scale-len(s)+1) + s
- }
- return sign + s[:len(s)-scale] + "." + s[len(s)-scale:]
- }
- // ===== Index page reading =====
- func rIP(pid uint32, it byte) *IP {
- b, e := rp(pid)
- if e != nil {
- return nil
- }
- if b[0] != PG_IDATA {
- return nil
- }
- lf := b[28] == 1
- cn := binary.LittleEndian.Uint32(b[24:28])
- el := int(binary.LittleEndian.Uint16(b[22:24]))
- if el <= 4 {
- el = 12
- }
- ents := pEE(b, int(cn), el, it, lf)
- us := float64(binary.LittleEndian.Uint32(b[16:20])) / float64(PGSZ) * 100
- return &IP{pid, binary.LittleEndian.Uint32(b[PH_NEXT : PH_NEXT+4]), binary.LittleEndian.Uint32(b[PH_PREV : PH_PREV+4]),
- lf, cn, us, ents, it}
- }
- func pEE(b []byte, cn, el int, it byte, lf bool) []IE {
- var ents []IE
- for i := 0; i < cn; i++ {
- o := IDX_HDR + i*el
- if o+el > len(b) {
- break
- }
- entry := b[o : o+el]
- var ie IE
- switch it {
- case 2:
- ie.K = fmt.Sprintf("%d", binary.BigEndian.Uint64(entry[:8]))
- ie.P = binary.BigEndian.Uint32(entry[8:12])
- case 1:
- ie.K = fmt.Sprintf("%d ~ %d", binary.BigEndian.Uint64(entry[:8]), binary.BigEndian.Uint64(entry[8:16]))
- ie.P = binary.BigEndian.Uint32(entry[16:20])
- case 3:
- ie.K = strings.TrimRight(string(entry[:16]), "\x00")
- ie.DP = binary.BigEndian.Uint64(entry[16:24])
- if lf {
- ie.P = binary.BigEndian.Uint32(entry[20:24])
- }
- case 4:
- nk := (el - 8) / 8
- parts := make([]string, nk)
- for j := 0; j < nk; j++ {
- parts[j] = fmt.Sprintf("%d", binary.BigEndian.Uint64(entry[j*8:]))
- }
- ie.K = strings.Join(parts, ", ")
- ie.DP = binary.BigEndian.Uint64(entry[nk*8:])
- if lf {
- ie.P = binary.BigEndian.Uint32(entry[el-4:])
- }
- case 5:
- if lf {
- kl := el - 8
- if kl == 8 {
- ie.K = fmt.Sprintf("%d", binary.BigEndian.Uint64(entry[:8]))
- } else {
- ie.K = fmt.Sprintf("%x", entry[:kl])
- }
- ie.DP = binary.BigEndian.Uint64(entry[kl:])
- ie.P = binary.BigEndian.Uint32(entry[el-4:])
- } else {
- kl := el - 4
- if kl == 8 {
- ie.K = fmt.Sprintf("%d", binary.BigEndian.Uint64(entry[:8]))
- } else {
- ie.K = fmt.Sprintf("%x", entry[:kl])
- }
- ie.P = binary.BigEndian.Uint32(entry[el-4:])
- }
- default:
- ie.K = fmt.Sprintf("%x", entry[:el-4])
- ie.P = binary.BigEndian.Uint32(entry[el-4:])
- }
- ents = append(ents, ie)
- }
- return ents
- }
- func rBT(pid uint32, it byte) *IP { return rIP(pid, it) }
- // ===== Header + Heap + Free =====
- func rHdrInfo() *HdrInfo {
- f, e := dbFile()
- if e != nil {
- return nil
- }
- b := make([]byte, HDRSZ)
- f.ReadAt(b, 0)
- // v11 起 HeaderPage 重排 (标量 8 字节对齐、零填充; 布局见 DB/core/header_page.go):
- // 0 Magic | 4 Version | 8 PageSize | 12 NextFreePage
- // 16 CreateTime | 24 ModifyTime | 32 NextTxnID | 40 GlobalFlags
- // 48 TablePage | 52 TableCount | 56 FreePageStart | 60 FreePageEnd
- // 64 FreePageCount | 68 FirstWALPage | 72 LastWALPage
- // 76 ExtConfigHdr[8] | 84 HeaderCrc | 88 ExtConfig[244]
- // 332 Comment[3764] (占满整页余下空间)
- magic := string(b[:4])
- ver := binary.LittleEndian.Uint32(b[4:8])
- pgsz := binary.LittleEndian.Uint32(b[8:12])
- nf := binary.LittleEndian.Uint32(b[12:16])
- ct := int64(binary.LittleEndian.Uint64(b[16:24]))
- mt := int64(binary.LittleEndian.Uint64(b[24:32]))
- txn := binary.LittleEndian.Uint64(b[32:40])
- gf := binary.LittleEndian.Uint64(b[40:48])
- tp := binary.LittleEndian.Uint32(b[48:52])
- tc := binary.LittleEndian.Uint32(b[52:56])
- fs := binary.LittleEndian.Uint32(b[56:60])
- fe := binary.LittleEndian.Uint32(b[HDR_FREE_END : HDR_FREE_END+4])
- fc := binary.LittleEndian.Uint32(b[64:68])
- wal0 := binary.LittleEndian.Uint32(b[68:72])
- wal1 := binary.LittleEndian.Uint32(b[72:76])
- cm := strings.TrimRight(string(b[332:4096]), "\x00")
- crc := le32(b[HDR_CRC : HDR_CRC+4])
- crcV, crcD := checkHeaderCrc(b, ver)
- fcW := rFreeChain()
- total := totalPages()
- h := &HdrInfo{
- Magic: magic, Version: ver, PageSize: pgsz,
- NextFree: nf, TablePage: tp, TableCount: tc,
- CreateTime: time.Unix(0, ct).Format("2006-01-02 15:04:05"),
- ModifyTime: time.Unix(0, mt).Format("2006-01-02 15:04:05"),
- FreeStart: fs, FreeEnd: fe, FreeCount: fc,
- GlobalFlags: gf, WALFirst: wal0, WALLast: wal1, TxnID: txn, Comment: cm,
- HeaderCrc: crc, CrcVerdict: crcV, CrcDetail: crcD,
- FreeWalked: fcW.Total, Pages: total,
- FreeVerdict: fcW.Verdict, FreeDetail: fcW.Detail,
- }
- if nf > 0 {
- h.Allocated = int(nf) - 1
- }
- if h.Allocated > total {
- h.Allocated = total
- }
- if h.NeverUsed = total - h.Allocated; h.NeverUsed < 0 {
- h.NeverUsed = 0
- }
- return h
- }
- func rHeapChain(hp0 uint32) *HeapChain {
- var pages []HeapPageInfo
- for pid := hp0; pid != 0; {
- b, e := rp(pid)
- if e != nil || b[0] != PG_HEAP {
- break
- }
- nx := binary.LittleEndian.Uint32(b[PH_NEXT : PH_NEXT+4])
- pv := binary.LittleEndian.Uint32(b[PH_PREV : PH_PREV+4])
- use := binary.LittleEndian.Uint32(b[PH_USE : PH_USE+4])
- us := float64(use) / float64(PGSZ) * 100
- ref := binary.LittleEndian.Uint16(b[PH_REF : PH_REF+2])
- pages = append(pages, HeapPageInfo{pid, nx, pv, use, us, ref})
- if nx == pid || nx == 0 {
- break
- }
- pid = nx
- }
- return &HeapChain{pages, len(pages)}
- }
- // rFreePages 空闲链 —— 直接用审计里那份带自检的版本。
- // 旧实现读的是偏移 0 的大端值, 而空闲链指针其实写在页头 NextPage (偏移 12);
- // 偏移 0 是 Type(空闲页=0) 那一格, 恒等于 0, 所以链永远只走出第 1 页。
- func rFreePages() *FreeChainInfo { return rFreeChain() }
- // ===== Page visualization =====
- type PageItem struct {
- ID uint32 `json:"id"`
- Type int `json:"t"`
- TS string `json:"ts"`
- Us float64 `json:"us"`
- }
- type PagesResult struct {
- Pages []PageItem `json:"pages"`
- Total int `json:"total"`
- Counts map[string]int `json:"counts"`
- Allocated int `json:"allocated"`
- NeverUsed int `json:"neverUsed"`
- Role map[string]string `json:"role"`
- Owner map[string]string `json:"owner"`
- }
- // rPages 页映射的**一个分片** —— 类型/使用率取自缓存的紧凑索引 (pagemap.go),
- // 角色/属主只为这一段页查询。
- //
- // 旧实现每次调用都要全量重算 19.5 万页, 并把整张页表 + 两张 19.5 万键的归属
- // map 一次性序列化 (单次响应几十 MB), 且被概览/堆链/空闲页三个视图各调一遍 ——
- // 前端必然卡死。新的页图走 /api/pagemap 分块拉取, 这里只保留分页语义。
- func rPages(start, count int) *PagesResult {
- idx := pagemapCached()
- res := &PagesResult{
- Total: idx.total, Counts: idx.counts, Allocated: idx.allocated,
- NeverUsed: idx.neverUsed, Role: map[string]string{}, Owner: map[string]string{},
- }
- if idx.total <= 0 {
- return res
- }
- if start < 1 {
- start = 1
- }
- if count <= 0 || count > pmChunkMax {
- count = pmChunkDefault
- }
- if start > idx.total {
- start = idx.total + 1
- }
- end := start + count
- if end > idx.total+1 {
- end = idx.total + 1
- }
- w := peekWalk()
- for pid := start; pid < end; pid++ {
- pt, tn := pmKind(int(idx.typ[pid]))
- res.Pages = append(res.Pages, PageItem{uint32(pid), pt, tn, float64(idx.usg[pid])})
- if w == nil {
- continue
- }
- k := strconv.Itoa(pid)
- if v, ok := w.role[uint32(pid)]; ok {
- res.Role[k] = v
- }
- if v, ok := w.owner[uint32(pid)]; ok {
- res.Owner[k] = v
- }
- }
- return res
- }
- // pagesH 分页返回页映射: ?start=<首页号>&count=<页数> (默认第一块)。
- func pagesH(w http.ResponseWriter, r *http.Request) {
- if fp == "" {
- jw(w, map[string]any{"e": "no db"})
- return
- }
- start, _ := strconv.Atoi(r.URL.Query().Get("start"))
- count, _ := strconv.Atoi(r.URL.Query().Get("count"))
- jw(w, rPages(start, count))
- }
- // findPkH 通过主键 B+ 树查找 PKID 对应的 DataPage
- func findPkH(w http.ResponseWriter, r *http.Request) {
- if fp == "" {
- jw(w, map[string]any{"e": "no db"})
- return
- }
- pkStr := r.URL.Query().Get("pk")
- rootStr := r.URL.Query().Get("root")
- var pk, root uint32
- fmt.Sscanf(pkStr, "%d", &pk)
- fmt.Sscanf(rootStr, "%d", &root)
- if root == 0 {
- jw(w, map[string]any{"e": "no root"})
- return
- }
- dp := findDPByPK(root, uint64(pk))
- if dp == 0 {
- jw(w, map[string]any{"e": "not found"})
- return
- }
- jw(w, map[string]any{"dp": dp, "pk": pk})
- }
- // findDPByPK 在主键 B+ 树中查找 PKID 对应的 DataPage ID
- func findDPByPK(rootPID uint32, targetPK uint64) uint32 {
- cur := rootPID
- for {
- b, e := rp(cur)
- if e != nil || b[0] != PG_IDATA {
- return 0
- }
- lf := b[28] == 1
- cn := int(binary.LittleEndian.Uint32(b[24:28]))
- el := int(binary.LittleEndian.Uint16(b[22:24]))
- if el <= 4 {
- el = 12
- }
- if lf {
- // 叶子节点: 查找匹配的 PK
- for i := 0; i < cn; i++ {
- o := IDX_HDR + i*el
- if o+el > len(b) {
- break
- }
- key := binary.BigEndian.Uint64(b[o : o+8])
- if key == targetPK {
- return binary.BigEndian.Uint32(b[o+8 : o+12])
- }
- }
- return 0
- }
- // 内部节点: 二分查找最后一个 key <= targetPK
- lo, hi := 0, cn-1
- pos := 0
- for lo <= hi {
- mid := (lo + hi) / 2
- o := IDX_HDR + mid*el
- if o+8 > len(b) {
- break
- }
- key := binary.BigEndian.Uint64(b[o : o+8])
- if key <= targetPK {
- pos = mid
- lo = mid + 1
- } else {
- hi = mid - 1
- }
- }
- o := IDX_HDR + pos*el
- childPID := binary.BigEndian.Uint32(b[o+el-4 : o+el])
- if childPID == 0 {
- return 0
- }
- cur = childPID
- }
- }
- // ===== Utility =====
- func min(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
- func fB(n int64) string {
- if n < 1024 {
- return fmt.Sprintf("%d B", n)
- }
- if n < 1048576 {
- return fmt.Sprintf("%.1f KB", float64(n)/1024)
- }
- if n < 1073741824 {
- return fmt.Sprintf("%.2f MB", float64(n)/1048576)
- }
- return fmt.Sprintf("%.2f GB", float64(n)/1073741824)
- }
- func fE(p string) bool { _, e := os.Stat(p); return e == nil }
- func jw(w http.ResponseWriter, v any) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(v)
- }
- // ===== HTTP Handlers =====
- func ldH(w http.ResponseWriter, r *http.Request) {
- p := r.URL.Query().Get("path")
- if p == "" {
- p, _ = os.Getwd()
- }
- if !filepath.IsAbs(p) {
- c, _ := os.Getwd()
- p = filepath.Join(c, p)
- }
- es, e := os.ReadDir(p)
- if e != nil {
- jw(w, map[string]any{"err": e.Error()})
- return
- }
- type F struct {
- N string `json:"n"`
- D bool `json:"d"`
- S int64 `json:"s"`
- }
- var ds, fs []F
- for _, x := range es {
- i, _ := x.Info()
- s := int64(0)
- if i != nil {
- s = i.Size()
- }
- if x.IsDir() {
- ds = append(ds, F{x.Name(), true, s})
- } else {
- fs = append(fs, F{x.Name(), false, s})
- }
- }
- jw(w, map[string]any{"p": p, "pp": filepath.Dir(p), "d": ds, "f": fs})
- }
- func openH(w http.ResponseWriter, r *http.Request) {
- if r.Method != "POST" {
- http.Error(w, "no", 405)
- return
- }
- var q struct {
- P string `json:"p"`
- }
- json.NewDecoder(r.Body).Decode(&q)
- if !fE(q.P) {
- c, _ := os.Getwd()
- q2 := filepath.Join(c, q.P)
- if fE(q2) {
- q.P = q2
- } else {
- jw(w, map[string]any{"ok": false, "e": "not found"})
- return
- }
- }
- // 待恢复 WAL 的库: 先拷到临时副本、用引擎打开一次 (触发 WAL 重放 + 孤儿 WAL
- // 自愈), 再分析副本。本工具是只读审计, 自己从不重放 —— 直接分析裸字节会把
- // "未重放"误报成一堆 CORRUPT (页版本错位 / free-live / missing-entry)。
- orig := q.P
- replayNote := ""
- if rp, err := replayWALToTemp(q.P); err != nil {
- replayNote = "wal replay failed: " + err.Error()
- } else if rp != "" {
- q.P = rp
- replayNote = "WAL 已在临时副本上重放, 审计对象为副本"
- }
- // 切库: 换掉共享句柄, 作废审计缓存
- if e := useDB(q.P); e != nil {
- jw(w, map[string]any{"ok": false, "e": "open failed: " + e.Error()})
- return
- }
- invalidateAudit()
- mu.Lock()
- fp = q.P
- rHdr()
- tabs = pTabs()
- mu.Unlock()
- fi, _ := os.Stat(fp)
- dbF = fp
- setCacheFP(fingerprint())
- // 全库审计可能很慢 (要扫上千页), 放到后台预热, 让打开请求立即返回。
- // 前端先显示基础信息与"审计中", 就绪后轮询 /api/stats 填充汇总。
- startAuditAsync()
- // 页映射索引同样在后台建好 (排在审计之后), 免得用户点开概览时再等一次全库扫描。
- startPagemapAsync()
- m := map[string]any{"ok": true, "s": st(fi)}
- if orig != q.P {
- m["orig"] = orig
- }
- if replayNote != "" {
- m["note"] = replayNote
- }
- jw(w, m)
- }
- // replayWALToTemp 库头里挂着未恢复 WAL (FirstWALPage != 0) 时, 把文件拷到临时
- // 副本并用引擎打开一次 —— Open 会自动重放 WAL (连同孤儿 WAL 链自愈), Close 后
- // 副本即是重放后的最终状态, 审计它才不会把"未重放"误报成一堆 CORRUPT。
- // 审计只读、永不写原库: 重放发生在【副本】上, 原文件一个字节不动。
- // 无需重放返回 (""); 副本路径按 源路径+大小+mtime 命名, 同一份库反复打开时复用。
- func replayWALToTemp(src string) (string, error) {
- f, err := os.Open(src)
- if err != nil {
- return "", err
- }
- hb := make([]byte, 4096)
- _, rerr := f.ReadAt(hb, 0)
- f.Close()
- if rerr != nil {
- return "", rerr
- }
- if binary.LittleEndian.Uint32(hb[68:72]) == 0 {
- return "", nil // 无待恢复 WAL
- }
- fi, _ := os.Stat(src)
- sum := sha1.Sum([]byte(fmt.Sprintf("%s|%d|%d", src, fi.Size(), fi.ModTime().UnixNano())))
- dst := filepath.Join(os.TempDir(), "tsdb2-analyzer-replay-"+hex.EncodeToString(sum[:8])+".db")
- if !fE(dst) {
- in, err := os.Open(src)
- if err != nil {
- return "", err
- }
- out, err := os.Create(dst)
- if err != nil {
- in.Close()
- return "", err
- }
- _, cerr := io.Copy(out, in)
- in.Close()
- out.Close()
- if cerr != nil {
- return "", cerr
- }
- }
- // 引擎打开副本: RecoverWAL + 孤儿 WAL 自愈在这里跑完, Close 落盘。
- db, err := core.OpenWithOptions(dst, core.OpenOptions{})
- if err != nil {
- return "", err
- }
- if err := db.Close(); err != nil {
- return "", err
- }
- return dst, nil
- }
- func st(fi os.FileInfo) map[string]any {
- s := int64(0)
- if fi != nil {
- s = fi.Size()
- }
- m := map[string]any{"f": dbF, "s": s, "fs": fB(s), "pg": totalPages(), "tc": len(tabs)}
- // 概览页要用到审计汇总 (空闲页数 / 堆页数 / 行数 / 总判决 / 孤儿页)。
- // 这里只读已算好的缓存, 不触发计算: 未就绪时返回 auditReady=false,
- // 由前端显示"审计中"并轮询本接口, 避免打开库时长时间卡死。
- //
- // 增量审计: 结构结果先就绪, 索引内容一致性随后补全。总判决里含索引内容,
- // 所以只有索引内容也补全了 (IdxPending=false) 才算真正就绪。
- if a := peekAudit(); a != nil && !a.IdxPending {
- heap, rows := 0, uint64(0)
- for _, t := range a.Tables {
- heap += t.HeapActual
- rows += t.Rows
- }
- m["free"] = a.Free.Total
- m["heap"] = heap
- m["rows"] = rows
- m["verdict"] = a.Verdict
- m["unacc"] = a.Reach.Unaccounted
- m["reserved"] = a.Reserved
- m["never"] = a.NeverUsed
- m["auditReady"] = true
- } else {
- m["auditReady"] = false
- if failed, msg := peekAuditError(); failed {
- m["auditFailed"] = true
- m["auditError"] = msg
- }
- }
- return m
- }
- func stH(w http.ResponseWriter, r *http.Request) {
- if dbF == "" {
- jw(w, map[string]any{"e": "no db"})
- return
- }
- fi, _ := os.Stat(fp)
- jw(w, st(fi))
- }
- // memH 报告 Go 运行时内存与映射区大小, 用来回答"内存到底花在哪"。
- // ?gc=1 强制一次 GC 并归还内存, 便于区分"活对象"与"待回收/未归还"。
- func memH(w http.ResponseWriter, r *http.Request) {
- if r.URL.Query().Get("gc") == "1" {
- runtime.GC()
- debug.FreeOSMemory()
- }
- var m runtime.MemStats
- runtime.ReadMemStats(&m)
- // 库文件大小: 现在按页 ReadAt 读取, 页留在系统文件缓存里 (可回收),
- // 不再整文件映射进进程地址空间, 因此这里不再有 mappedDBSZ 这一项。
- var fileMB int64
- if fi, e := os.Stat(fp); e == nil {
- fileMB = fi.Size() >> 20
- }
- jw(w, map[string]any{
- "heapAllocMB": m.HeapAlloc >> 20, // 活对象
- "heapInuseMB": m.HeapInuse >> 20, // 已用 span
- "heapIdleMB": m.HeapIdle >> 20, // 空闲 span (可归还)
- "heapSysMB": m.HeapSys >> 20, // 向 OS 要的堆
- "stackMB": m.StackInuse >> 20, // goroutine 栈
- "otherSysMB": m.OtherSys >> 20,
- "sysMB": m.Sys >> 20, // Go 全部向 OS 要的内存
- "dbFileMB": fileMB, // 库文件大小 (按页 ReadAt, 不进进程工作集)
- "numGC": m.NumGC,
- "goroutines": runtime.NumGoroutine(),
- })
- }
- func taH(w http.ResponseWriter, r *http.Request) { jw(w, tabs) }
- func tdH(w http.ResponseWriter, r *http.Request) {
- n := r.URL.Path[len("/api/table/"):]
- for _, t := range tabs {
- if t.Name == n {
- jw(w, t)
- return
- }
- }
- jw(w, map[string]any{"e": "not found"})
- }
- func dpH(w http.ResponseWriter, r *http.Request) {
- var pid uint32
- fmt.Sscanf(r.URL.Query().Get("id"), "%d", &pid)
- d := rDP(pid)
- if d == nil {
- jw(w, map[string]any{"e": "err"})
- return
- }
- jw(w, d)
- }
- func ipH(w http.ResponseWriter, r *http.Request) {
- var pid uint32
- fmt.Sscanf(r.URL.Query().Get("id"), "%d", &pid)
- it, _ := strconv.Atoi(r.URL.Query().Get("t"))
- d := rIP(pid, byte(it))
- if d == nil {
- jw(w, map[string]any{"e": "err"})
- return
- }
- jw(w, d)
- }
- func btH(w http.ResponseWriter, r *http.Request) {
- var pid uint32
- fmt.Sscanf(r.URL.Query().Get("id"), "%d", &pid)
- it, _ := strconv.Atoi(r.URL.Query().Get("t"))
- d := rBT(pid, byte(it))
- if d == nil {
- jw(w, map[string]any{"e": "err"})
- return
- }
- jw(w, d)
- }
- func hdrH(w http.ResponseWriter, r *http.Request) {
- d := rHdrInfo()
- if d == nil {
- jw(w, map[string]any{"e": "err"})
- return
- }
- jw(w, d)
- }
- func heapH(w http.ResponseWriter, r *http.Request) {
- var hp0 uint32
- fmt.Sscanf(r.URL.Query().Get("id"), "%d", &hp0)
- jw(w, rHeapChain(hp0))
- }
- func freeH(w http.ResponseWriter, r *http.Request) { jw(w, rFreePages()) }
- // ===== 审计 / 归属 / 单页检查 =====
- // auditH 全库审计: 页账 + 空闲链自检 + 每表堆链交叉校验 + 未归属页。
- func auditH(w http.ResponseWriter, r *http.Request) {
- if dbF == "" {
- jw(w, map[string]any{"e": "no db"})
- return
- }
- jw(w, auditCached())
- }
- // pageArgs 解析分页参数: offset 默认 0, limit 默认 100 (上限 500)。
- func pageArgs(r *http.Request) (int, int) {
- off, _ := strconv.Atoi(r.URL.Query().Get("offset"))
- lim, _ := strconv.Atoi(r.URL.Query().Get("limit"))
- if off < 0 {
- off = 0
- }
- if lim <= 0 || lim > 500 {
- lim = 100
- }
- return off, lim
- }
- // auditIssuesH 分页取索引问题明细 —— 从落盘缓存库里按页取, 不整份读进内存。
- func auditIssuesH(w http.ResponseWriter, r *http.Request) {
- off, lim := pageArgs(r)
- rows, total, err := cachePageIssues(off, lim)
- if err != nil {
- jw(w, map[string]any{"e": err.Error()})
- return
- }
- jw(w, map[string]any{"rows": rows, "total": total, "offset": off, "limit": lim})
- }
- // auditUnaccH 分页取孤儿页明细 —— 同上, 从缓存库按页取。
- func auditUnaccH(w http.ResponseWriter, r *http.Request) {
- off, lim := pageArgs(r)
- rows, total, err := cachePageUnacc(off, lim)
- if err != nil {
- jw(w, map[string]any{"e": err.Error()})
- return
- }
- jw(w, map[string]any{"rows": rows, "total": total, "offset": off, "limit": lim})
- }
- // heapChainsH 所有表的堆链汇总 (声明值 / 实走值 / 引用数), 供"堆链总览"页。
- func heapChainsH(w http.ResponseWriter, r *http.Request) {
- if dbF == "" {
- jw(w, map[string]any{"e": "no db"})
- return
- }
- a := auditCached()
- jw(w, map[string]any{"tables": a.Tables, "free": a.Free, "reach": a.Reach, "verdict": a.Verdict})
- }
- // pageInfoH 单页检查: 原始页头 + 归属 + 是否空闲 / 从未分配。
- func pageInfoH(w http.ResponseWriter, r *http.Request) {
- if dbF == "" {
- jw(w, map[string]any{"e": "no db"})
- return
- }
- var pid uint32
- fmt.Sscanf(r.URL.Query().Get("id"), "%d", &pid)
- if pid == 0 {
- jw(w, map[string]any{"e": "bad id"})
- return
- }
- jw(w, rPageInfo(pid))
- }
- var _ = unsafe.Sizeof(0)
|