Last active
August 8, 2021 15:06
-
-
Save GreenLightning/93cbc69eb6216dc0a1ef01576fd9b8fe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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