Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save GreenLightning/93cbc69eb6216dc0a1ef01576fd9b8fe to your computer and use it in GitHub Desktop.
Save GreenLightning/93cbc69eb6216dc0a1ef01576fd9b8fe to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
)
type Executable struct {
Shortname string
Filename string
Durations []time.Duration
}
// This script measures the startup time of gui applications on Windows.
// It measures the time between process creation and a window being shown
// (i.e. initialization code that runs after the window is shown is not measured).
//
// Example invocation: go run record.go -iterations 40 a:C:\Users\Green\Desktop\a.exe b:C:\Users\Green\Desktop\b.exe
//
// This will launch the a.exe and b.exe executables, alternating between the two,
// until each one has been started 40 times. After that, the startup times in seconds
// will be written to data/a.txt and data/b.txt.
//
// The number of executables is dynamic, just change the number of
// <shortname>:<executable> items on the command line.
//
// The script alternates between the executables to evenly distribute the
// effects of any long-term performance disturbances (e.g. thermal throttling).
//
func main() {
iterationsFlag := flag.Int("iterations", 1, "how many times to start each executable")
flag.Parse()
iterations := *iterationsFlag
var executables []*Executable
for _, name := range flag.Args() {
index := strings.Index(name, ":")
if index == -1 {
fmt.Println("invalid argument:", name)
fmt.Println("usage: <shortname>:<executable>...")
return
}
executables = append(executables, &Executable{
Shortname: name[:index],
Filename: name[index+1:],
Durations: make([]time.Duration, 0, iterations),
})
}
if len(executables) == 0 {
fmt.Println("usage: [-iterations 1] <shortname>:<executable>...")
return
}
for _, executable := range executables {
_, err := os.Stat(executable.Filename)
if err != nil {
fmt.Println(err)
return
}
}
var targetPid int
var findHandle syscall.Handle
var findErr error
callback := syscall.NewCallback(func(h syscall.Handle, p uintptr) uintptr {
var pid uint32
_, err := GetWindowThreadProcessId(h, &pid)
if err != nil {
findErr = err
return 0
}
visible, err := IsWindowVisible(h)
if err != nil {
findErr = err
return 0
}
if int(pid) == targetPid && visible {
findHandle = h
return 0
}
return 1
})
for i := 0; i < iterations; i++ {
for _, executable := range executables {
cmd := exec.Command(executable.Filename)
t0 := time.Now()
err := cmd.Start()
if err != nil {
panic(err)
}
targetPid = cmd.Process.Pid
findHandle = 0
findErr = nil
for findHandle == 0 && findErr == nil {
EnumWindows(callback, 0)
}
if findErr != nil {
panic(findErr)
}
t1 := time.Now()
duration := t1.Sub(t0)
executable.Durations = append(executable.Durations, duration)
fmt.Println(duration)
time.Sleep(500 * time.Millisecond)
err = cmd.Process.Kill()
if err != nil {
panic(err)
}
time.Sleep(1500 * time.Millisecond)
}
}
dataDir := "data"
err := os.MkdirAll(dataDir, 0644)
if err != nil {
panic(err)
}
for _, executable := range executables {
var buffer bytes.Buffer
fmt.Fprintf(&buffer, "## Filename: %s\n\n", executable.Filename)
for _, duration := range executable.Durations {
d := float64(duration) / float64(time.Second)
fmt.Fprintf(&buffer, "%f\n", d)
}
filename := filepath.Join(dataDir, fmt.Sprintf("%s.txt", executable.Shortname))
err := os.WriteFile(filename, buffer.Bytes(), 0755)
if err != nil {
fmt.Println(err)
}
}
}
var (
user32 = syscall.MustLoadDLL("user32.dll")
procEnumWindows = user32.MustFindProc("EnumWindows")
procIsWindowVisible = user32.MustFindProc("IsWindowVisible")
procGetWindowThreadProcessId = user32.MustFindProc("GetWindowThreadProcessId")
)
func EnumWindows(enumFunc uintptr, lparam uintptr) (err error) {
r, _, e := syscall.Syscall(procEnumWindows.Addr(), 2, uintptr(enumFunc), uintptr(lparam), 0)
if r == 0 {
if e != 0 {
err = e
} else {
err = syscall.EINVAL
}
}
return
}
func GetWindowThreadProcessId(hwnd syscall.Handle, pid *uint32) (tid uint32, err error) {
r, _, e := syscall.Syscall(procGetWindowThreadProcessId.Addr(), 2, uintptr(hwnd), uintptr(unsafe.Pointer(pid)), 0)
tid = uint32(r)
if tid == 0 {
err = e
}
return
}
func IsWindowVisible(handle syscall.Handle) (visible bool, err error) {
r, _, e := syscall.Syscall(procIsWindowVisible.Addr(), 1, uintptr(handle), 0, 0)
visible = r != 0
if e != 0 {
err = e
}
return
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment