mirror of
https://github.com/criyle/go-judge.git
synced 2025-11-04 14:50:02 +08:00
fixed typo
This commit is contained in:
parent
ecaadcc36e
commit
ce8a78454f
@ -13,5 +13,5 @@ type Task interface {
|
|||||||
// it should sent received work through go channel (have background goroutine(s))
|
// it should sent received work through go channel (have background goroutine(s))
|
||||||
type Client interface {
|
type Client interface {
|
||||||
// C should return channel to receive works
|
// C should return channel to receive works
|
||||||
C() <-chan types.JudgeTask
|
C() <-chan Task
|
||||||
}
|
}
|
||||||
|
|||||||
5
go.mod
5
go.mod
@ -2,4 +2,7 @@ module github.com/criyle/go-judge
|
|||||||
|
|
||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require github.com/criyle/go-sandbox v0.0.0-20190902024918-1df3228aec4d
|
require (
|
||||||
|
github.com/criyle/go-sandbox v0.0.0-20190906040622-be668a5e7548
|
||||||
|
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03 // indirect
|
||||||
|
)
|
||||||
|
|||||||
9
go.sum
9
go.sum
@ -1,5 +1,6 @@
|
|||||||
github.com/criyle/go-sandbox v0.0.0-20190902024918-1df3228aec4d h1:JpdxvAKkHYskHPGuVm7hPnndOBuCygkOxc1p8V1rPeI=
|
github.com/criyle/go-sandbox v0.0.0-20190906040622-be668a5e7548 h1:5JzsTB6UUHCoZkgjBmU68WYhIdbs189xtkUHJs+qu4c=
|
||||||
github.com/criyle/go-sandbox v0.0.0-20190902024918-1df3228aec4d/go.mod h1:RZ6jui4DXDFZHojTt5pNxr6FXUbCcW+TLITDSANJllk=
|
github.com/criyle/go-sandbox v0.0.0-20190906040622-be668a5e7548/go.mod h1:gkn5TWck4A8kKs8xX0q1+yLA0WnV2e7ECm/OmYigDCI=
|
||||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||||
golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 h1:7z820YPX9pxWR59qM7BE5+fglp4D/mKqAwCvGt11b+8=
|
golang.org/x/sys v0.0.0-20190904005037-43c01164e931/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03 h1:b3JiLYVaG9kHjTcOQIoUh978YMCO7oVTQQBLudU47zY=
|
||||||
|
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|||||||
@ -1 +1,14 @@
|
|||||||
package judger
|
package judger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/criyle/go-judge/client"
|
||||||
|
"github.com/criyle/go-judge/problem"
|
||||||
|
"github.com/criyle/go-judge/taskqueue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Judger receives task from client and translate to task for runner
|
||||||
|
type Judger struct {
|
||||||
|
client.Client
|
||||||
|
taskqueue.Queue
|
||||||
|
problem.Builder
|
||||||
|
}
|
||||||
|
|||||||
138
judger/loop.go
Normal file
138
judger/loop.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package judger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/criyle/go-judge/client"
|
||||||
|
"github.com/criyle/go-judge/file"
|
||||||
|
"github.com/criyle/go-judge/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Loop fetch judge task from client and report results
|
||||||
|
// in a infinite loop
|
||||||
|
func (j *Judger) Loop(done <-chan struct{}) {
|
||||||
|
c := j.Client.C()
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case t := <-c:
|
||||||
|
rt := j.run(done, t)
|
||||||
|
t.Finish(rt)
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
break loop
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Judger) run(done <-chan struct{}, t client.Task) types.JudgeResult {
|
||||||
|
var result types.JudgeResult
|
||||||
|
errResult := func(err error) types.JudgeResult {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
p := t.Param()
|
||||||
|
pconf, err := j.Build(p.TestData)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile
|
||||||
|
compileRet := make(chan types.RunTaskResult)
|
||||||
|
err = j.Enqueue(types.RunTask{
|
||||||
|
Type: "compile",
|
||||||
|
Language: p.Language,
|
||||||
|
Code: p.Code,
|
||||||
|
ExtraFiles: pconf.ExtraFiles,
|
||||||
|
}, compileRet)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err)
|
||||||
|
}
|
||||||
|
compileTaskResult := <-compileRet
|
||||||
|
if compileTaskResult.Error != "" {
|
||||||
|
return errResult(err)
|
||||||
|
}
|
||||||
|
execFiles := compileTaskResult.ExecFiles
|
||||||
|
|
||||||
|
// run
|
||||||
|
subTaskResult := make(chan types.JudgeSubTaskResult, len(pconf.Subtasks))
|
||||||
|
pj := problemJudger{
|
||||||
|
Judger: j,
|
||||||
|
ProblemConfig: &pconf,
|
||||||
|
Task: t,
|
||||||
|
JudgeTask: p,
|
||||||
|
total: count(&pconf),
|
||||||
|
}
|
||||||
|
for _, s := range pconf.Subtasks {
|
||||||
|
s := &s
|
||||||
|
go func() {
|
||||||
|
subTaskResult <- pj.runSubtask(done, execFiles, s)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for range pconf.Subtasks {
|
||||||
|
result.SubTasks = append(result.SubTasks, <-subTaskResult)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type problemJudger struct {
|
||||||
|
*Judger
|
||||||
|
*types.ProblemConfig
|
||||||
|
*types.JudgeTask
|
||||||
|
client.Task
|
||||||
|
count int32
|
||||||
|
total int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pj *problemJudger) runSubtask(done <-chan struct{}, exec []file.File, s *types.SubTask) types.JudgeSubTaskResult {
|
||||||
|
var result types.JudgeSubTaskResult
|
||||||
|
caseResult := make(chan types.RunTaskResult, len(s.Cases))
|
||||||
|
for _, c := range s.Cases {
|
||||||
|
pj.Enqueue(types.RunTask{
|
||||||
|
Type: pj.ProblemConfig.Type,
|
||||||
|
Language: pj.Language,
|
||||||
|
TimeLimit: pj.TileLimit,
|
||||||
|
MemoryLimit: pj.MemoryLimit,
|
||||||
|
Executables: exec,
|
||||||
|
InputFile: c.Input,
|
||||||
|
AnswerFile: c.Answer,
|
||||||
|
}, caseResult)
|
||||||
|
}
|
||||||
|
for range s.Cases {
|
||||||
|
rt := <-caseResult
|
||||||
|
result.Cases = append(result.Cases, types.JudgeCaseResult{
|
||||||
|
Status: rt.Status,
|
||||||
|
ScoreRate: rt.ScoringRate,
|
||||||
|
Error: rt.Error,
|
||||||
|
Time: rt.Time,
|
||||||
|
Memory: rt.Memory,
|
||||||
|
Input: rt.Input,
|
||||||
|
Answer: rt.Answer,
|
||||||
|
UserOutput: rt.UserOutput,
|
||||||
|
UserError: rt.UserError,
|
||||||
|
SpjOutput: rt.SpjOutput,
|
||||||
|
})
|
||||||
|
result.Score += rt.ScoringRate
|
||||||
|
// report prograss
|
||||||
|
atomic.AddInt32(&pj.count, 1)
|
||||||
|
pj.Progress(types.JudgeProgress{
|
||||||
|
Message: fmt.Sprintf("%d/%d", pj.count, pj.total),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// count counts total number of cases
|
||||||
|
func count(pconf *types.ProblemConfig) int32 {
|
||||||
|
var count int32
|
||||||
|
for _, s := range pconf.Subtasks {
|
||||||
|
count += int32(len(s.Cases))
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
@ -1,15 +1,26 @@
|
|||||||
package language
|
package language
|
||||||
|
|
||||||
|
// Type defines compile / exec
|
||||||
|
type Type int
|
||||||
|
|
||||||
|
// Defines the exec type
|
||||||
|
const (
|
||||||
|
TypeCompile Type = iota + 1
|
||||||
|
TypeExec
|
||||||
|
)
|
||||||
|
|
||||||
// Language defines the way to run program
|
// Language defines the way to run program
|
||||||
type Language interface {
|
type Language interface {
|
||||||
Get(string, string) ExecParam // Get execparam for specific language and type (compile / run)
|
Get(string, Type) ExecParam // Get execparam for specific language and type (compile / run)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecParam defines specs to compile / run program
|
// ExecParam defines specs to compile / run program
|
||||||
type ExecParam struct {
|
type ExecParam struct {
|
||||||
SourceFileName string
|
|
||||||
Args []string
|
Args []string
|
||||||
CompiledFileNames []string
|
|
||||||
|
// Compile
|
||||||
|
SourceFileName string // put code when compile
|
||||||
|
CompiledFileNames []string // exec files
|
||||||
|
|
||||||
// limits
|
// limits
|
||||||
TimeLimit uint64
|
TimeLimit uint64
|
||||||
|
|||||||
1
pkg/diff/diff.go
Normal file
1
pkg/diff/diff.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package diff
|
||||||
11
problem/interface.go
Normal file
11
problem/interface.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package problem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/criyle/go-judge/file"
|
||||||
|
"github.com/criyle/go-judge/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder builds problem specs from file
|
||||||
|
type Builder interface {
|
||||||
|
Build([]file.File) (types.ProblemConfig, error)
|
||||||
|
}
|
||||||
@ -3,11 +3,11 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/deamon"
|
"github.com/criyle/go-sandbox/daemon"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pool struct {
|
type pool struct {
|
||||||
queue chan *deamon.Master
|
queue chan *daemon.Master
|
||||||
count int32
|
count int32
|
||||||
root string
|
root string
|
||||||
}
|
}
|
||||||
@ -16,26 +16,26 @@ const maxPoolSize = 64
|
|||||||
|
|
||||||
func newPool(root string) *pool {
|
func newPool(root string) *pool {
|
||||||
return &pool{
|
return &pool{
|
||||||
queue: make(chan *deamon.Master, maxPoolSize),
|
queue: make(chan *daemon.Master, maxPoolSize),
|
||||||
root: root,
|
root: root,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) Get() (*deamon.Master, error) {
|
func (p *pool) Get() (*daemon.Master, error) {
|
||||||
select {
|
select {
|
||||||
case m := <-p.queue:
|
case m := <-p.queue:
|
||||||
return m, nil
|
return m, nil
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
atomic.AddInt32(&p.count, 1)
|
atomic.AddInt32(&p.count, 1)
|
||||||
return deamon.New(p.root)
|
return daemon.New(p.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) Put(master *deamon.Master) {
|
func (p *pool) Put(master *daemon.Master) {
|
||||||
p.queue <- master
|
p.queue <- master
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) Destroy(master *deamon.Master) {
|
func (p *pool) Destroy(master *daemon.Master) {
|
||||||
master.Destroy()
|
master.Destroy()
|
||||||
atomic.AddInt32(&p.count, -1)
|
atomic.AddInt32(&p.count, -1)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/deamon"
|
"github.com/criyle/go-sandbox/daemon"
|
||||||
"github.com/criyle/go-sandbox/pkg/cgroup"
|
"github.com/criyle/go-sandbox/pkg/cgroup"
|
||||||
"github.com/criyle/go-sandbox/pkg/pipe"
|
"github.com/criyle/go-sandbox/pkg/pipe"
|
||||||
stypes "github.com/criyle/go-sandbox/types"
|
stypes "github.com/criyle/go-sandbox/types"
|
||||||
|
|
||||||
|
"github.com/criyle/go-judge/file"
|
||||||
|
"github.com/criyle/go-judge/file/memfile"
|
||||||
|
"github.com/criyle/go-judge/language"
|
||||||
"github.com/criyle/go-judge/types"
|
"github.com/criyle/go-judge/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,7 +23,11 @@ const checkIntervalMS = 50
|
|||||||
var env = []string{"PATH=/usr/local/bin:/usr/bin:/bin"}
|
var env = []string{"PATH=/usr/local/bin:/usr/bin:/bin"}
|
||||||
|
|
||||||
func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskResult {
|
func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskResult {
|
||||||
param := r.Language.Get(task.Language, task.Type)
|
t := language.TypeExec
|
||||||
|
if task.Type == "compile" {
|
||||||
|
t = language.TypeCompile
|
||||||
|
}
|
||||||
|
param := r.Language.Get(task.Language, t)
|
||||||
|
|
||||||
// init input / output / error files
|
// init input / output / error files
|
||||||
inputFile, err := task.InputFile.Open()
|
inputFile, err := task.InputFile.Open()
|
||||||
@ -47,10 +55,10 @@ func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskRe
|
|||||||
}
|
}
|
||||||
defer cg.Destroy()
|
defer cg.Destroy()
|
||||||
|
|
||||||
// get deamon runner
|
// get daemon runner
|
||||||
m, err := r.pool.Get()
|
m, err := r.pool.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errResult("failed to get deamon instance")
|
return errResult("failed to get daemon instance")
|
||||||
}
|
}
|
||||||
defer r.pool.Put(m)
|
defer r.pool.Put(m)
|
||||||
|
|
||||||
@ -64,7 +72,7 @@ func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskRe
|
|||||||
cg.SetPidsMax(param.ProcLimit)
|
cg.SetPidsMax(param.ProcLimit)
|
||||||
|
|
||||||
// set running parameters
|
// set running parameters
|
||||||
execParam := deamon.ExecveParam{
|
execParam := daemon.ExecveParam{
|
||||||
Args: param.Args,
|
Args: param.Args,
|
||||||
Envv: env,
|
Envv: env,
|
||||||
Fds: []uintptr{inputFile.Fd(), outputPipe.W.Fd(), errorPipe.W.Fd()},
|
Fds: []uintptr{inputFile.Fd(), outputPipe.W.Fd(), errorPipe.W.Fd()},
|
||||||
@ -72,7 +80,7 @@ func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cancellable signal channel
|
// cancellable signal channel
|
||||||
cancelC := newCancelableChannel()
|
cancelC := newCancellableChannel()
|
||||||
defer cancelC.cancel()
|
defer cancelC.cancel()
|
||||||
|
|
||||||
// start the process
|
// start the process
|
||||||
@ -169,6 +177,27 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
inputContent, _ := task.InputFile.Content()
|
inputContent, _ := task.InputFile.Content()
|
||||||
|
|
||||||
|
// If compile read compiled files
|
||||||
|
var exec []file.File
|
||||||
|
if task.Type == "compile" {
|
||||||
|
for _, fn := range param.CompiledFileNames {
|
||||||
|
f, err := m.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error())
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
c, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error())
|
||||||
|
}
|
||||||
|
exec = append(exec, memfile.New(fn, c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: diff
|
||||||
|
|
||||||
return &types.RunTaskResult{
|
return &types.RunTaskResult{
|
||||||
Status: status,
|
Status: status,
|
||||||
Time: cpuUsage / uint64(time.Millisecond),
|
Time: cpuUsage / uint64(time.Millisecond),
|
||||||
@ -176,6 +205,7 @@ loop:
|
|||||||
Input: inputContent,
|
Input: inputContent,
|
||||||
UserOutput: outputPipe.Buffer.Bytes(),
|
UserOutput: outputPipe.Buffer.Bytes(),
|
||||||
UserError: errorPipe.Buffer.Bytes(),
|
UserError: errorPipe.Buffer.Bytes(),
|
||||||
|
ExecFiles: exec,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,18 +216,18 @@ func errResult(err string) *types.RunTaskResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type cancelableChannel struct {
|
type cancellableChannel struct {
|
||||||
Done chan struct{}
|
Done chan struct{}
|
||||||
canceled bool
|
canceled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCancelableChannel() *cancelableChannel {
|
func newCancellableChannel() *cancellableChannel {
|
||||||
return &cancelableChannel{
|
return &cancellableChannel{
|
||||||
Done: make(chan struct{}),
|
Done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cancelableChannel) cancel() {
|
func (c *cancellableChannel) cancel() {
|
||||||
if !c.canceled {
|
if !c.canceled {
|
||||||
close(c.Done)
|
close(c.Done)
|
||||||
c.canceled = true
|
c.canceled = true
|
||||||
|
|||||||
50
taskqueue/channel/channel.go
Normal file
50
taskqueue/channel/channel.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/criyle/go-judge/taskqueue"
|
||||||
|
"github.com/criyle/go-judge/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const buffSize = 512
|
||||||
|
|
||||||
|
// Queue implements taskqueue by go channel
|
||||||
|
type Queue struct {
|
||||||
|
queue chan taskqueue.Task
|
||||||
|
}
|
||||||
|
|
||||||
|
// New craetes new Queue with buffed go channel
|
||||||
|
func New() *Queue {
|
||||||
|
return &Queue{
|
||||||
|
queue: make(chan taskqueue.Task, buffSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue puts task into run queue
|
||||||
|
func (q *Queue) Enqueue(t types.RunTask, r chan<- types.RunTaskResult) error {
|
||||||
|
q.queue <- Task{
|
||||||
|
task: t,
|
||||||
|
result: r,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// C returns the underlying channel
|
||||||
|
func (q *Queue) C() <-chan taskqueue.Task {
|
||||||
|
return q.queue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task implements Task interface
|
||||||
|
type Task struct {
|
||||||
|
task types.RunTask
|
||||||
|
result chan<- types.RunTaskResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task returns task parameters
|
||||||
|
func (t Task) Task() *types.RunTask {
|
||||||
|
return &t.task
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish returns the run task result
|
||||||
|
func (t Task) Finish(r *types.RunTaskResult) {
|
||||||
|
t.result <- *r
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ type JudgeProgress struct {
|
|||||||
// JudgeResult contains final result of current task
|
// JudgeResult contains final result of current task
|
||||||
type JudgeResult struct {
|
type JudgeResult struct {
|
||||||
SubTasks []JudgeSubTaskResult
|
SubTasks []JudgeSubTaskResult
|
||||||
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
// JudgeSubTaskResult contains result for single sub-task
|
// JudgeSubTaskResult contains result for single sub-task
|
||||||
|
|||||||
@ -25,6 +25,6 @@ type SubTask struct {
|
|||||||
|
|
||||||
// Case defines single judge case
|
// Case defines single judge case
|
||||||
type Case struct {
|
type Case struct {
|
||||||
Input []file.File
|
Input file.File
|
||||||
Answer []file.File
|
Answer file.File
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,8 @@ type RunTaskResult struct {
|
|||||||
Status string
|
Status string
|
||||||
// error
|
// error
|
||||||
Error string
|
Error string
|
||||||
|
// compile result
|
||||||
|
ExecFiles []file.File
|
||||||
// details
|
// details
|
||||||
Time uint64 // ms
|
Time uint64 // ms
|
||||||
Memory uint64 // kb
|
Memory uint64 // kb
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user