github.com/arnodel/golua@v0.0.0-20230215163904-e0b5347eaaa1/runtime/runtimecontextmanager.go (about) 1 //go:build !noquotas 2 // +build !noquotas 3 4 package runtime 5 6 import ( 7 "fmt" 8 "strings" 9 "time" 10 11 "github.com/arnodel/golua/runtime/internal/luagc" 12 ) 13 14 const QuotasAvailable = true 15 16 // When tracking time, the runtimeContextManager will take the opportunity to 17 // update the time spent in the context when the CPU is increased. It doesn't 18 // do that every time because it would slow down the runtime too much for little 19 // benefit. This value sets the amount of CPU required that triggers a time 20 // update. 21 const cpuThresholdIncrement = 10000 22 23 type runtimeContextManager struct { 24 hardLimits RuntimeResources 25 softLimits RuntimeResources 26 usedResources RuntimeResources 27 28 requiredFlags ComplianceFlags 29 30 status RuntimeContextStatus 31 32 parent *runtimeContextManager 33 34 messageHandler Callable 35 36 trackCpu bool 37 trackMem bool 38 trackTime bool 39 stopLevel StopLevel 40 startTime uint64 41 nextCpuThreshold uint64 42 43 weakRefPool luagc.Pool 44 gcPolicy GCPolicy 45 } 46 47 var _ RuntimeContext = (*runtimeContextManager)(nil) 48 49 func (m *runtimeContextManager) initRoot() { 50 m.gcPolicy = IsolateGCPolicy 51 m.weakRefPool = luagc.NewDefaultPool() 52 } 53 54 func (m *runtimeContextManager) HardLimits() RuntimeResources { 55 return m.hardLimits 56 } 57 58 func (m *runtimeContextManager) SoftLimits() RuntimeResources { 59 return m.softLimits 60 } 61 62 func (m *runtimeContextManager) UsedResources() RuntimeResources { 63 return m.usedResources 64 } 65 66 func (m *runtimeContextManager) Status() RuntimeContextStatus { 67 return m.status 68 } 69 70 func (m *runtimeContextManager) setStatus(st RuntimeContextStatus) { 71 m.status = st 72 } 73 74 func (m *runtimeContextManager) RequiredFlags() ComplianceFlags { 75 return m.requiredFlags 76 } 77 78 func (m *runtimeContextManager) CheckRequiredFlags(flags ComplianceFlags) error { 79 missingFlags := m.requiredFlags &^ flags 80 if missingFlags != 0 { 81 return fmt.Errorf("missing flags: %s", strings.Join(missingFlags.Names(), " ")) 82 } 83 return nil 84 } 85 86 func (m *runtimeContextManager) Parent() RuntimeContext { 87 return m.parent 88 } 89 90 func (m *runtimeContextManager) SetStopLevel(stopLevel StopLevel) { 91 m.stopLevel |= stopLevel 92 if stopLevel&HardStop != 0 && m.status == StatusLive { 93 m.KillContext() 94 } 95 } 96 97 func (m *runtimeContextManager) Due() bool { 98 return m.stopLevel&SoftStop != 0 || !m.softLimits.Dominates(m.usedResources) 99 } 100 101 func (m *runtimeContextManager) RuntimeContext() RuntimeContext { 102 return m 103 } 104 105 func (m *runtimeContextManager) PushContext(ctx RuntimeContextDef) { 106 if m.trackTime { 107 m.updateTimeUsed() 108 } 109 parent := *m 110 m.startTime = now() 111 m.hardLimits = m.hardLimits.Remove(m.usedResources).Merge(ctx.HardLimits) 112 m.softLimits = m.hardLimits.Merge(m.softLimits).Merge(ctx.SoftLimits) 113 m.usedResources = RuntimeResources{} 114 m.requiredFlags |= ctx.RequiredFlags 115 116 if ctx.HardLimits.Cpu > 0 { 117 m.requiredFlags |= ComplyCpuSafe 118 } 119 if ctx.HardLimits.Memory > 0 { 120 m.requiredFlags |= ComplyMemSafe 121 } 122 if ctx.HardLimits.Millis > 0 { 123 m.requiredFlags |= ComplyTimeSafe 124 } 125 m.trackTime = m.hardLimits.Millis > 0 || m.softLimits.Millis > 0 126 m.trackCpu = m.hardLimits.Cpu > 0 || m.softLimits.Cpu > 0 || m.trackTime 127 m.trackMem = m.hardLimits.Memory > 0 || m.softLimits.Memory > 0 128 m.status = StatusLive 129 m.messageHandler = ctx.MessageHandler 130 m.parent = &parent 131 if ctx.GCPolicy == IsolateGCPolicy || ctx.HardLimits.Millis > 0 || ctx.HardLimits.Cpu > 0 || ctx.HardLimits.Memory > 0 { 132 m.weakRefPool = luagc.NewDefaultPool() 133 m.gcPolicy = IsolateGCPolicy 134 } else { 135 m.weakRefPool = parent.weakRefPool 136 m.gcPolicy = ShareGCPolicy 137 } 138 } 139 140 func (m *runtimeContextManager) GCPolicy() GCPolicy { 141 return m.gcPolicy 142 } 143 144 func (m *runtimeContextManager) PopContext() RuntimeContext { 145 if m == nil || m.parent == nil { 146 return nil 147 } 148 if m.gcPolicy == IsolateGCPolicy { 149 m.weakRefPool.ExtractAllMarkedFinalize() 150 releaseResources(m.weakRefPool.ExtractAllMarkedRelease()) 151 } 152 mCopy := *m 153 if mCopy.status == StatusLive { 154 mCopy.status = StatusDone 155 } 156 m.parent.RequireCPU(m.usedResources.Cpu) 157 m.parent.RequireMem(m.usedResources.Memory) 158 *m = *m.parent 159 if m.trackTime { 160 m.updateTimeUsed() 161 } 162 return &mCopy 163 } 164 165 func (m *runtimeContextManager) RequireCPU(cpuAmount uint64) { 166 if m.trackCpu { 167 // The path with limit is "outlined" so RequireCPU can be inlined, 168 // minimising the overhead when there is no limit. 169 m.requireCPU(cpuAmount) 170 } 171 } 172 173 //go:noinline 174 func (m *runtimeContextManager) requireCPU(cpuAmount uint64) { 175 if m.stopLevel&HardStop != 0 { 176 m.KillContext() 177 } 178 cpuUsed := m.usedResources.Cpu + cpuAmount 179 if atLimit(cpuUsed, m.hardLimits.Cpu) { 180 m.TerminateContext("CPU limit of %d exceeded", m.hardLimits.Cpu) 181 } 182 if m.trackTime && m.nextCpuThreshold <= cpuUsed { 183 m.nextCpuThreshold = cpuUsed + cpuThresholdIncrement 184 m.updateTimeUsed() 185 } 186 m.usedResources.Cpu = cpuUsed 187 } 188 189 func (m *runtimeContextManager) UnusedCPU() uint64 { 190 return m.hardLimits.Cpu - m.usedResources.Cpu 191 } 192 193 func (m *runtimeContextManager) RequireMem(memAmount uint64) { 194 if m.trackMem { 195 // The path with limit is "outlined" so RequireMem can be inlined, 196 // minimising the overhead when there is no limit. 197 m.requireMem(memAmount) 198 } 199 } 200 201 //go:noinline 202 func (m *runtimeContextManager) requireMem(memAmount uint64) { 203 if m.stopLevel&HardStop != 0 { 204 m.KillContext() 205 } 206 memUsed := m.usedResources.Memory + memAmount 207 if atLimit(memUsed, m.hardLimits.Memory) { 208 m.TerminateContext("memory limit of %d exceeded", m.hardLimits.Memory) 209 } 210 m.usedResources.Memory = memUsed 211 } 212 213 func (m *runtimeContextManager) RequireSize(sz uintptr) (mem uint64) { 214 mem = uint64(sz) 215 m.RequireMem(mem) 216 return 217 } 218 219 func (m *runtimeContextManager) RequireArrSize(sz uintptr, n int) (mem uint64) { 220 mem = uint64(sz) * uint64(n) 221 m.RequireMem(mem) 222 return 223 } 224 225 func (m *runtimeContextManager) RequireBytes(n int) (mem uint64) { 226 mem = uint64(n) 227 m.RequireMem(mem) 228 return 229 } 230 231 func (m *runtimeContextManager) ReleaseMem(memAmount uint64) { 232 // TODO: think about what to do when memory is released when unwinding from 233 // a quota exceeded error 234 if m.hardLimits.Memory > 0 { 235 if memAmount <= m.usedResources.Memory { 236 m.usedResources.Memory -= memAmount 237 } else { 238 panic("Too much mem released") 239 } 240 } 241 } 242 243 func (m *runtimeContextManager) ReleaseSize(sz uintptr) { 244 m.ReleaseMem(uint64(sz)) 245 } 246 247 func (m *runtimeContextManager) ReleaseArrSize(sz uintptr, n int) { 248 m.ReleaseMem(uint64(sz) * uint64(n)) 249 } 250 251 func (m *runtimeContextManager) ReleaseBytes(n int) { 252 m.ReleaseMem(uint64(n)) 253 } 254 255 func (m *runtimeContextManager) UnusedMem() uint64 { 256 return m.hardLimits.Memory - m.usedResources.Memory 257 } 258 259 func (m *runtimeContextManager) updateTimeUsed() { 260 m.usedResources.Millis = now() - m.startTime 261 if atLimit(m.usedResources.Millis, m.hardLimits.Millis) { 262 m.TerminateContext("time limit of %d exceeded", m.hardLimits.Millis) 263 } 264 } 265 266 // LinearUnused returns an amount of resource combining memory and cpu. It is 267 // useful when calling functions whose time complexity is a linear function of 268 // the size of their output. As cpu ticks are "smaller" than memory ticks, the 269 // cpuFactor arguments allows specifying an increased "weight" for cpu ticks. 270 func (m *runtimeContextManager) LinearUnused(cpuFactor uint64) uint64 { 271 mem := m.UnusedMem() 272 cpu := m.UnusedCPU() * cpuFactor 273 switch { 274 case cpu == 0: 275 return mem 276 case mem == 0: 277 return cpu 278 case cpu > mem: 279 return mem 280 default: 281 return cpu 282 } 283 } 284 285 // LinearRequire can be used to actually consume (part of) the resource budget 286 // returned by LinearUnused (with the same cpuFactor). 287 func (m *runtimeContextManager) LinearRequire(cpuFactor uint64, amt uint64) { 288 m.RequireMem(amt) 289 m.RequireCPU(amt / cpuFactor) 290 } 291 292 // KillContext forcefully terminates the context with the message "force kill". 293 func (m *runtimeContextManager) KillContext() { 294 m.TerminateContext("force kill") 295 } 296 297 // TerminateContext forcefully terminates the context with the given message. 298 func (m *runtimeContextManager) TerminateContext(format string, args ...interface{}) { 299 if m.status != StatusLive { 300 return 301 } 302 m.status = StatusKilled 303 panic(ContextTerminationError{ 304 message: fmt.Sprintf(format, args...), 305 }) 306 } 307 308 // Current unix time in ms 309 func now() uint64 { 310 return uint64(time.Now().UnixNano() / 1e6) 311 }