Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions ease_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ func Test_RunApp_Run_WithFailedVQM(t *testing.T) {
wantErrMsg := "VQM calculations had errors, see log for reasons"
wantExitCode := 1
gotErr := app.Run([]string{"-plan", plan, "-out-dir", outDir})
assert.NotNil(t, gotErr)
assert.ErrorContains(t, gotErr, wantErrMsg)

gotExitCode := gotErr.(*AppError).ExitCode()
var appErr *AppError
assert.ErrorAs(t, gotErr, &appErr)

gotExitCode := appErr.ExitCode()
assert.Equal(t, wantExitCode, gotExitCode, "Exit code mismatch")
}

Expand All @@ -135,7 +139,10 @@ func Test_RunApp_Run_WithInvalidPlanConfigParseError(t *testing.T) {
gotErr := app.Run([]string{"-plan", fixPlanConfigInvalid(t), "-out-dir", t.TempDir()})
assert.ErrorContains(t, gotErr, wantErrMsg)

gotExitCode := gotErr.(*AppError).ExitCode()
var appErr *AppError
assert.ErrorAs(t, gotErr, &appErr)

gotExitCode := appErr.ExitCode()
assert.Equal(t, wantExitCode, gotExitCode, "Exit code mismatch")
}

Expand All @@ -155,8 +162,11 @@ func Test_RunApp_Run_WithNonEmptyOutDirShouldTerminate(t *testing.T) {
wantErrMsg := "non-empty out dir"
assert.ErrorContains(t, gotErr, wantErrMsg)

var appErr *AppError
assert.ErrorAs(t, gotErr, &appErr)

wantExitCode := 1
gotExitCode := gotErr.(*AppError).ExitCode()
gotExitCode := appErr.ExitCode()
assert.Equal(t, wantExitCode, gotExitCode, "Exit code mismatch")
}

Expand Down
37 changes: 3 additions & 34 deletions internal/encoding/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/evolution-gaming/ease/internal/logging"
"github.com/evolution-gaming/ease/internal/lw"
"github.com/evolution-gaming/ease/internal/metric"
"github.com/evolution-gaming/ease/internal/tools"
)

Expand Down Expand Up @@ -93,7 +94,7 @@ func (s *EncoderCmd) Run() RunResult {
logging.Debugf("Stderr: %s", buf.Bytes())
r.AddError(err)
}
r.Stats = NewUsageStat(time.Since(start), r.Rusage())
r.Stats = metric.NewUsageStat(time.Since(start), r.Rusage())
// Add VideoDuration and also calculate approximation to average encoding speed.
vmeta, err := tools.FfprobeExtractMetadata(r.CompressedFile)
if err != nil {
Expand Down Expand Up @@ -283,7 +284,7 @@ type RunResult struct {
Errors []error
cmd *exec.Cmd
stderr []byte
Stats UsageStat
Stats metric.UsageStat
VideoDuration float64
AvgEncodingSpeed float64
}
Expand All @@ -310,38 +311,6 @@ func (s *RunResult) AddError(e error) {
s.Errors = append(s.Errors, e)
}

// UsageStat contains process resource usage stats.
type UsageStat struct {
// Human friendly representations of time duration
HStime string
HUtime string
HElapsed string
// time.Duration is nanoseconds
Stime time.Duration
Utime time.Duration
Elapsed time.Duration
// MaxRss is KB
MaxRss int64
}

// NewUsageStat will create UsageStat instance.
func NewUsageStat(elapsed time.Duration, rusage *syscall.Rusage) UsageStat {
return UsageStat{
Stime: time.Duration(syscall.TimevalToNsec(rusage.Stime)),
Utime: time.Duration(syscall.TimevalToNsec(rusage.Utime)),
Elapsed: elapsed,
HStime: time.Duration(syscall.TimevalToNsec(rusage.Stime)).String(),
HUtime: time.Duration(syscall.TimevalToNsec(rusage.Utime)).String(),
HElapsed: elapsed.String(),
MaxRss: rusage.Maxrss,
}
}

// CPUPercent calculates CPU usage in percent.
func (s *UsageStat) CPUPercent() float64 {
return float64(s.Stime+s.Utime) / float64(s.Elapsed) * 100
}

// generateOutputFileNameBase will generate a sensible output filename without extension.
func generateOutputFileNameBase(inputFile, outDir, postfix string) string {
// Normalize filename strings to sane format (no spaces).
Expand Down
3 changes: 3 additions & 0 deletions internal/metric/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ type Record struct {
Stime time.Duration
Utime time.Duration
Elapsed time.Duration
VQMStime time.Duration
VQMUtime time.Duration
VQMElapsed time.Duration
MaxRss int64
VideoDuration float64
AvgEncodingSpeed float64
Expand Down
43 changes: 43 additions & 0 deletions internal/metric/usage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright ©2025 Evolution. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// Process usage stats, mostly wrapping relevant fields from syscall.Rusage.
package metric

import (
"syscall"
"time"
)

// UsageStat contains process resource usage stats.
type UsageStat struct {
// Human friendly representations of time duration
HStime string
HUtime string
HElapsed string
// time.Duration is nanoseconds
Stime time.Duration
Utime time.Duration
Elapsed time.Duration
// MaxRss is KB
MaxRss int64
}

// NewUsageStat will create UsageStat instance.
func NewUsageStat(elapsed time.Duration, rusage *syscall.Rusage) UsageStat {
return UsageStat{
Stime: time.Duration(syscall.TimevalToNsec(rusage.Stime)),
Utime: time.Duration(syscall.TimevalToNsec(rusage.Utime)),
Elapsed: elapsed,
HStime: time.Duration(syscall.TimevalToNsec(rusage.Stime)).String(),
HUtime: time.Duration(syscall.TimevalToNsec(rusage.Utime)).String(),
HElapsed: elapsed.String(),
MaxRss: rusage.Maxrss,
}
}

// CPUPercent calculates CPU usage in percent.
func (s *UsageStat) CPUPercent() float64 {
return float64(s.Stime+s.Utime) / float64(s.Elapsed) * 100
}
27 changes: 22 additions & 5 deletions internal/vqm/vqm.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import (
"os/exec"
"runtime"
"strings"
"syscall"
"text/template"
"time"

"github.com/evolution-gaming/ease/internal/logging"
"github.com/evolution-gaming/ease/internal/metric"
"github.com/evolution-gaming/ease/internal/tools"
"github.com/google/shlex"
"gonum.org/v1/gonum/floats"
Expand Down Expand Up @@ -101,6 +104,7 @@ type FfmpegVMAF struct {
resultFile string
output []byte
measured bool
usageStat metric.UsageStat
}

func (f *FfmpegVMAF) Measure() error {
Expand All @@ -124,6 +128,7 @@ func (f *FfmpegVMAF) Measure() error {
return fmt.Errorf("frame count mismatch: source %v != compressed %v", srcMeta.FrameCount, compressedMeta.FrameCount)
}

tStart := time.Now()
cmd := exec.Command(f.exePath, f.ffmpegArgs...) //#nosec G204
logging.Debugf("VQM tool command: %v", cmd.Args)
f.output, err = cmd.CombinedOutput()
Expand All @@ -132,11 +137,23 @@ func (f *FfmpegVMAF) Measure() error {
logging.Infof("VQM tool output:\n%s", f.output)
return fmt.Errorf("VQM calculation error: %w", err)
}
tDelta := time.Since(tStart)

rusage, ok := cmd.ProcessState.SysUsage().(*syscall.Rusage)
if !ok {
return errors.New("getting VQM calculation rusage")
}

f.usageStat = metric.NewUsageStat(tDelta, rusage)
f.measured = true

return nil
}

func (f *FfmpegVMAF) UsageStat() metric.UsageStat {
return f.usageStat
}

type AggregateMetric struct {
VMAF Metric
PSNR Metric
Expand Down Expand Up @@ -211,21 +228,21 @@ type ffmpegVMAFResult struct {
}

type frame struct {
FrameNum uint `json:"frameNum"`
Metrics metric `json:"metrics"`
FrameNum uint `json:"frameNum"`
Metrics frameMetric `json:"metrics"`
}

type metric struct {
type frameMetric struct {
VMAF float64
PSNR float64
MS_SSIM float64
}

// UnmarshalJSON implements json.Unmarshaler interface for metric.
// UnmarshalJSON implements json.Unmarshaler interface for frameMetric.
//
// A custom unmarshaler is needed to work around lack of stability around libvmaf measured
// VQ metric field names in output.
func (m *metric) UnmarshalJSON(b []byte) error {
func (m *frameMetric) UnmarshalJSON(b []byte) error {
// Ignore "null" as per convention.
if string(b) == "null" {
return nil
Expand Down
5 changes: 5 additions & 0 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func (a *App) encode(plan encoding.Plan) error {
logging.Infof("Error while getting metrics for %s: %s", record.CompressedFile, err)
continue
}
vqmUsageStat := vqmTool.UsageStat()

fs, err := analysis.GetFrameStats(record.CompressedFile, a.cfg.FfprobePath.Value())
if err != nil {
Expand All @@ -212,6 +213,10 @@ func (a *App) encode(plan encoding.Plan) error {

// Update record with VQ metrics.
record.VQMResultFile = resFile
record.VQMStime = vqmUsageStat.Stime
record.VQMUtime = vqmUsageStat.Utime
record.VQMElapsed = vqmUsageStat.Elapsed

record.PSNRMin = res.PSNR.Min
record.PSNRMax = res.PSNR.Max
record.PSNRMean = res.PSNR.Mean
Expand Down