mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 12:23:43 +00:00
233 lines
6.7 KiB
Go
233 lines
6.7 KiB
Go
//go:build darwin || linux
|
|
|
|
package evaluation
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type resourceSnapshot struct {
|
|
HeapAlloc uint64 `json:"heap_alloc_bytes"`
|
|
HeapInuse uint64 `json:"heap_inuse_bytes"`
|
|
HeapObjects uint64 `json:"heap_objects"`
|
|
RSS int64 `json:"rss_bytes"`
|
|
PeakRSS int64 `json:"process_peak_rss_bytes"`
|
|
Goroutines int `json:"goroutines"`
|
|
}
|
|
|
|
type resourceResult struct {
|
|
Engine string `json:"engine"`
|
|
Extension string `json:"extension"`
|
|
Mode string `json:"mode"`
|
|
Count int `json:"count"`
|
|
UserNS int64 `json:"user_cpu_ns"`
|
|
SystemNS int64 `json:"system_cpu_ns"`
|
|
WallNS int64 `json:"wall_ns"`
|
|
Allocated uint64 `json:"allocated_bytes"`
|
|
GCs uint32 `json:"gc_cycles"`
|
|
GCPause uint64 `json:"gc_pause_ns"`
|
|
OutputHash string `json:"output_sha256"`
|
|
Baseline resourceSnapshot `json:"baseline"`
|
|
AfterWork resourceSnapshot `json:"after_work"`
|
|
AfterGC resourceSnapshot `json:"after_gc"`
|
|
AfterOSRelease resourceSnapshot `json:"after_os_release"`
|
|
AfterClose resourceSnapshot `json:"after_close_os_release"`
|
|
}
|
|
|
|
// moejs releases weak-keyed compiled metadata via asynchronous Go cleanups.
|
|
// Multiple collections with scheduling opportunities avoid counting objects
|
|
// pending those cleanups as permanently live. This is outside CPU timing.
|
|
func settledGC(t *testing.T) {
|
|
t.Helper()
|
|
var previous uint64
|
|
for i := 0; i < 8; i++ {
|
|
runtime.GC()
|
|
time.Sleep(20 * time.Millisecond)
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
delta := m.HeapAlloc
|
|
if previous > m.HeapAlloc {
|
|
delta = previous - m.HeapAlloc
|
|
} else {
|
|
delta = m.HeapAlloc - previous
|
|
}
|
|
if i >= 2 && delta < 64*1024 {
|
|
return
|
|
}
|
|
previous = m.HeapAlloc
|
|
}
|
|
t.Fatal("live heap did not settle within eight GC rounds")
|
|
}
|
|
|
|
func selfUsage(t *testing.T) syscall.Rusage {
|
|
t.Helper()
|
|
var usage syscall.Rusage
|
|
if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return usage
|
|
}
|
|
|
|
func timevalNS(v syscall.Timeval) int64 { return int64(v.Sec)*1e9 + int64(v.Usec)*1e3 }
|
|
|
|
func snapshot(t *testing.T) resourceSnapshot {
|
|
t.Helper()
|
|
var mem runtime.MemStats
|
|
runtime.ReadMemStats(&mem)
|
|
// ps runs only outside the timed CPU interval. RUSAGE_SELF excludes its CPU.
|
|
b, err := exec.Command("ps", "-o", "rss=", "-p", strconv.Itoa(os.Getpid())).Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rss, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
peak := int64(selfUsage(t).Maxrss)
|
|
if runtime.GOOS == "linux" {
|
|
peak *= 1024
|
|
}
|
|
return resourceSnapshot{mem.HeapAlloc, mem.HeapInuse, mem.HeapObjects, rss * 1024, peak, runtime.NumGoroutine()}
|
|
}
|
|
|
|
func resourceRuntime(t *testing.T, engine, name, src, host string) Runtime {
|
|
t.Helper()
|
|
r, err := New(engine)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Clone the bundle for each runtime, like a fresh production file read.
|
|
for _, s := range []string{fmt.Sprintf("var extensionName=%q;", name), host, strings.Clone(src)} {
|
|
if _, err = r.Eval(s); err != nil {
|
|
r.Close()
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func resolveResource(t *testing.T, r Runtime) string {
|
|
t.Helper()
|
|
value, err := r.Eval("resolveOnce()")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
t.Fatalf("unexpected resolve result %T", value)
|
|
}
|
|
sum := sha256.Sum256([]byte(text))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// Each invocation is a fresh OS process. The Python driver repeats both engines
|
|
// in alternating order; the ordinary test suite skips this measurement worker.
|
|
func TestResourceWorker(t *testing.T) {
|
|
if os.Getenv("GOPEED_RESOURCE_WORKER") != "1" {
|
|
t.Skip("process resource measurement worker")
|
|
}
|
|
engine, name, mode := os.Getenv("RESOURCE_ENGINE"), os.Getenv("RESOURCE_EXTENSION"), os.Getenv("RESOURCE_MODE")
|
|
count, err := strconv.Atoi(os.Getenv("RESOURCE_COUNT"))
|
|
if err != nil || count < 1 || count > 10000 {
|
|
t.Fatal("invalid RESOURCE_COUNT")
|
|
}
|
|
src := source(t, name)
|
|
hostBytes, err := os.ReadFile("replay.js")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
host := string(hostBytes)
|
|
// Pay one-time global initialization only before timing repeat events.
|
|
// Retained-memory runs start without a priming runtime so their memory
|
|
// includes the selected engine's first-use caches and builtins.
|
|
var hash string
|
|
if mode != "retained" {
|
|
prime := resourceRuntime(t, engine, name, src, host)
|
|
hash = resolveResource(t, prime)
|
|
prime.Close()
|
|
prime = nil
|
|
}
|
|
debug.FreeOSMemory()
|
|
result := resourceResult{Engine: engine, Extension: name, Mode: mode, Count: count, Baseline: snapshot(t)}
|
|
var retained []Runtime
|
|
if mode == "warm" {
|
|
r := resourceRuntime(t, engine, name, src, host)
|
|
if h := resolveResource(t, r); h != hash {
|
|
t.Fatal("warm output differs")
|
|
}
|
|
retained = append(retained, r)
|
|
debug.FreeOSMemory()
|
|
}
|
|
var before, after runtime.MemStats
|
|
runtime.ReadMemStats(&before)
|
|
cpuBefore := selfUsage(t)
|
|
started := time.Now()
|
|
for i := 0; i < count; i++ {
|
|
var r Runtime
|
|
switch mode {
|
|
case "warm":
|
|
r = retained[0]
|
|
case "cold", "retained":
|
|
r = resourceRuntime(t, engine, name, src, host)
|
|
default:
|
|
t.Fatal("unknown RESOURCE_MODE")
|
|
}
|
|
h := resolveResource(t, r)
|
|
if hash == "" {
|
|
hash = h
|
|
} else if h != hash {
|
|
t.Fatal("resolve output changed")
|
|
}
|
|
if mode == "retained" {
|
|
retained = append(retained, r)
|
|
} else if mode == "cold" {
|
|
r.Close()
|
|
}
|
|
}
|
|
result.WallNS = time.Since(started).Nanoseconds()
|
|
cpuAfter := selfUsage(t)
|
|
runtime.ReadMemStats(&after)
|
|
result.UserNS = timevalNS(cpuAfter.Utime) - timevalNS(cpuBefore.Utime)
|
|
result.SystemNS = timevalNS(cpuAfter.Stime) - timevalNS(cpuBefore.Stime)
|
|
result.Allocated = after.TotalAlloc - before.TotalAlloc
|
|
result.GCs = after.NumGC - before.NumGC
|
|
result.GCPause = after.PauseTotalNs - before.PauseTotalNs
|
|
result.AfterWork = snapshot(t)
|
|
settledGC(t)
|
|
result.AfterGC = snapshot(t)
|
|
debug.FreeOSMemory()
|
|
result.AfterOSRelease = snapshot(t)
|
|
runtime.KeepAlive(retained)
|
|
for _, r := range retained {
|
|
r.Close()
|
|
}
|
|
retained = nil
|
|
settledGC(t)
|
|
debug.FreeOSMemory()
|
|
result.AfterClose = snapshot(t)
|
|
// Keep input strings alive across all snapshots, including after closing
|
|
// runtimes, so memory deltas cannot accidentally subtract the input bundle.
|
|
runtime.KeepAlive(src)
|
|
runtime.KeepAlive(host)
|
|
result.OutputHash = hash
|
|
output, err := json.MarshalIndent(result, "", " ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = os.WriteFile(os.Getenv("RESOURCE_OUTPUT"), append(output, '\n'), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|