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  }