Open
Description
Go version
go version go1.23.2 windows/amd64
Output of go env
in your module/workspace:
set GO111MODULE=
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\hajimehoshi\AppData\Local\go-build
set GOENV=C:\Users\hajimehoshi\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=C:\Users\hajimehoshi\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\hajimehoshi\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.23.2
set GODEBUG=
set GOTELEMETRY=local
set GOTELEMETRYDIR=C:\Users\hajimehoshi\AppData\Roaming\go\telemetry
set GCCGO=gccgo
set GOAMD64=v1
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=C:\Users\hajimehoshi\ebiten\go.mod
set GOWORK=
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\HAJIME~1\AppData\Local\Temp\go-build1125585036=/tmp/go-build -gno-record-gcc-switches
What did you do?
Compile this Go program to an execute file (with -ldflags="-H=windowsgui"
)
EDIT: I could minized the case further. See #71242 (comment)
package main
import (
"log"
"os"
"runtime"
"runtime/debug"
"syscall"
"time"
"unsafe"
)
const (
WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000
CW_USEDEFAULT = ^0x7fffffff
SW_SHOW = 5
WM_DESTROY = 2
)
type (
ATOM uint16
HANDLE uintptr
HINSTANCE HANDLE
HICON HANDLE
HCURSOR HANDLE
HBRUSH HANDLE
HWND HANDLE
HMENU HANDLE
)
type WNDCLASSEX struct {
Size uint32
Style uint32
WndProc uintptr
ClsExtra int32
WndExtra int32
Instance HINSTANCE
Icon HICON
Cursor HCURSOR
Background HBRUSH
MenuName *uint16
ClassName *uint16
IconSm HICON
}
type RECT struct {
Left, Top, Right, Bottom int32
}
type POINT struct {
X, Y int32
}
type MSG struct {
Hwnd HWND
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt POINT
}
func GetModuleHandle(modulename *uint16) HINSTANCE {
r, _, _ := syscall.SyscallN(procGetModuleHandle.Addr(), uintptr(unsafe.Pointer(modulename)))
return HINSTANCE(r)
}
func RegisterClassEx(w *WNDCLASSEX) ATOM {
r, _, _ := syscall.SyscallN(procRegisterClassEx.Addr(), uintptr(unsafe.Pointer(w)))
return ATOM(r)
}
func CreateWindowEx(exStyle uint, className, windowName *uint16,
style uint, x, y, width, height int, parent HWND, menu HMENU,
instance HINSTANCE, param unsafe.Pointer) HWND {
r, _, _ := syscall.SyscallN(procCreateWindowEx.Addr(), uintptr(exStyle), uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(windowName)), uintptr(style), uintptr(x), uintptr(y), uintptr(width), uintptr(height),
uintptr(parent), uintptr(menu), uintptr(instance), uintptr(param))
return HWND(r)
}
func AdjustWindowRect(rect *RECT, style uint, menu bool) bool {
var iMenu uintptr
if menu {
iMenu = 1
}
r, _, _ := syscall.SyscallN(procAdjustWindowRect.Addr(), uintptr(unsafe.Pointer(rect)), uintptr(style), iMenu)
return r != 0
}
func ShowWindow(hwnd HWND, cmdshow int) bool {
r, _, _ := syscall.SyscallN(procShowWindow.Addr(), uintptr(hwnd), uintptr(cmdshow))
return r != 0
}
func GetMessage(msg *MSG, hwnd HWND, msgFilterMin, msgFilterMax uint32) int {
r, _, _ := syscall.SyscallN(procGetMessage.Addr(), uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))
return int(r)
}
func TranslateMessage(msg *MSG) bool {
r, _, _ := syscall.SyscallN(procTranslateMessage.Addr(), uintptr(unsafe.Pointer(msg)))
return r != 0
}
func DispatchMessage(msg *MSG) uintptr {
r, _, _ := syscall.SyscallN(procDispatchMessage.Addr(), uintptr(unsafe.Pointer(msg)))
return r
}
func DefWindowProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
r, _, _ := syscall.SyscallN(procDefWindowProc.Addr(), uintptr(hwnd), uintptr(msg), wParam, lParam)
return r
}
func PostQuitMessage(exitCode int) {
syscall.SyscallN(procPostQuitMessage.Addr(), uintptr(exitCode))
}
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
user32 = syscall.NewLazyDLL("user32.dll")
procRegisterClassEx = user32.NewProc("RegisterClassExW")
procCreateWindowEx = user32.NewProc("CreateWindowExW")
procAdjustWindowRect = user32.NewProc("AdjustWindowRect")
procShowWindow = user32.NewProc("ShowWindow")
procGetMessage = user32.NewProc("GetMessageW")
procTranslateMessage = user32.NewProc("TranslateMessage")
procDispatchMessage = user32.NewProc("DispatchMessageW")
procDefWindowProc = user32.NewProc("DefWindowProcW")
procPostQuitMessage = user32.NewProc("PostQuitMessage")
)
func init() {
runtime.LockOSThread()
}
func main() {
className, err := syscall.UTF16PtrFromString("Sample Window Class")
if err != nil {
panic(err)
}
inst := GetModuleHandle(className)
wc := WNDCLASSEX{
Size: uint32(unsafe.Sizeof(WNDCLASSEX{})),
WndProc: syscall.NewCallback(wndProc),
Instance: inst,
ClassName: className,
}
RegisterClassEx(&wc)
wr := RECT{
Left: 0,
Top: 0,
Right: 320,
Bottom: 240,
}
title, err := syscall.UTF16PtrFromString("My Title")
if err != nil {
panic(err)
}
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, false)
hwnd := CreateWindowEx(
0, className,
title,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, int(wr.Right-wr.Left), int(wr.Bottom-wr.Top),
0, 0, inst, nil,
)
if hwnd == 0 {
panic(syscall.GetLastError())
}
ShowWindow(hwnd, SW_SHOW)
go func() {
for {
_ = make([]byte, 256*1024)
time.Sleep(time.Millisecond)
}
}()
go func() {
f, err := os.Create("log.txt")
if err != nil {
panic(err)
}
defer f.Close()
log.SetOutput(f)
for {
time.Sleep(time.Second)
var gcStats debug.GCStats
debug.ReadGCStats(&gcStats)
log.Printf("LastGC: %s, NumGC: %d, PauseTotal: %s", gcStats.LastGC, gcStats.NumGC, gcStats.PauseTotal)
if err := f.Sync(); err != nil {
panic(err)
}
}
}()
var msg MSG
for GetMessage(&msg, 0, 0, 0) != 0 {
TranslateMessage(&msg)
DispatchMessage(&msg)
}
}
func wndProc(hwnd HWND, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case WM_DESTROY:
PostQuitMessage(0)
}
return DefWindowProc(hwnd, msg, wparam, lparam)
}
Replace an exe file in a Steam game with the compiled exe file, and run it via Steam client.
What did you see happen?
The application sometimes freezes for more than 10 seconds. For example, I saw this log
2025/01/13 23:23:41 LastGC: 2025-01-13 23:23:41.6060533 +0900 JST, NumGC: 51, PauseTotal: 1.3186ms
2025/01/13 23:23:42 LastGC: 2025-01-13 23:23:42.6470915 +0900 JST, NumGC: 102, PauseTotal: 4.5945ms
2025/01/13 23:23:43 LastGC: 2025-01-13 23:23:43.6434896 +0900 JST, NumGC: 152, PauseTotal: 7.8752ms
2025/01/13 23:23:44 LastGC: 2025-01-13 23:23:44.6481645 +0900 JST, NumGC: 204, PauseTotal: 10.224ms
2025/01/13 23:23:45 LastGC: 2025-01-13 23:23:45.6448025 +0900 JST, NumGC: 255, PauseTotal: 12.5067ms
2025/01/13 23:23:46 LastGC: 2025-01-13 23:23:46.6584686 +0900 JST, NumGC: 309, PauseTotal: 14.2881ms
2025/01/13 23:23:47 LastGC: 2025-01-13 23:23:47.6550747 +0900 JST, NumGC: 362, PauseTotal: 17.056ms
2025/01/13 23:23:48 LastGC: 2025-01-13 23:23:48.6681264 +0900 JST, NumGC: 413, PauseTotal: 18.5012ms
2025/01/13 23:23:49 LastGC: 2025-01-13 23:23:49.6577372 +0900 JST, NumGC: 464, PauseTotal: 21.2877ms
2025/01/13 23:23:50 LastGC: 2025-01-13 23:23:50.6675317 +0900 JST, NumGC: 514, PauseTotal: 24.9508ms
2025/01/13 23:23:51 LastGC: 2025-01-13 23:23:51.6642908 +0900 JST, NumGC: 563, PauseTotal: 27.2671ms
2025/01/13 23:23:52 LastGC: 2025-01-13 23:23:52.6696781 +0900 JST, NumGC: 612, PauseTotal: 29.6692ms
2025/01/13 23:23:53 LastGC: 2025-01-13 23:23:53.6818947 +0900 JST, NumGC: 661, PauseTotal: 31.9968ms
2025/01/13 23:23:54 LastGC: 2025-01-13 23:23:54.6830572 +0900 JST, NumGC: 711, PauseTotal: 34.9958ms
2025/01/13 23:23:55 LastGC: 2025-01-13 23:23:55.6889185 +0900 JST, NumGC: 761, PauseTotal: 38.2468ms
2025/01/13 23:23:56 LastGC: 2025-01-13 23:23:56.6869067 +0900 JST, NumGC: 813, PauseTotal: 41.5747ms
2025/01/13 23:23:57 LastGC: 2025-01-13 23:23:57.6920325 +0900 JST, NumGC: 863, PauseTotal: 45.5415ms
2025/01/13 23:24:18 LastGC: 2025-01-13 23:23:58.2810119 +0900 JST, NumGC: 894, PauseTotal: 47.2387ms
2025/01/13 23:24:19 LastGC: 2025-01-13 23:24:19.3442472 +0900 JST, NumGC: 945, PauseTotal: 51.3869ms
2025/01/13 23:24:20 LastGC: 2025-01-13 23:24:20.3460036 +0900 JST, NumGC: 995, PauseTotal: 54.0004ms
2025/01/13 23:24:21 LastGC: 2025-01-13 23:24:21.3371656 +0900 JST, NumGC: 1047, PauseTotal: 55.3437ms
2025/01/13 23:24:22 LastGC: 2025-01-13 23:24:22.344327 +0900 JST, NumGC: 1098, PauseTotal: 56.9757ms
2025/01/13 23:24:23 LastGC: 2025-01-13 23:24:23.3523815 +0900 JST, NumGC: 1147, PauseTotal: 61.8229ms
2025/01/13 23:24:24 LastGC: 2025-01-13 23:24:24.3560493 +0900 JST, NumGC: 1200, PauseTotal: 64.7476ms
2025/01/13 23:24:25 LastGC: 2025-01-13 23:24:25.3552534 +0900 JST, NumGC: 1250, PauseTotal: 67.5551ms
You can see a freeze happens between 22:23:57 and 22:24:18.
What did you expect to see?
The application doesn't freeze.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Todo
Milestone
Relationships
Development
No branches or pull requests
Activity
hajimehoshi commentedon Jan 13, 2025
This was originally reported at hajimehoshi/ebiten#3181 by @corfe83.
A Steam overlay might cause this issue but we are not sure.This freeze happens even when the Steam overlay is disabled.debug.SetGCPercent(-1)
gabyhelp commentedon Jan 13, 2025
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
hajimehoshi commentedon Jan 13, 2025
I could reproduce this even with a console application (without
-ldflags="-H=windowsgui"
).runtime.LockOSThread
is not needed. Replacing an exe in a Steam game is still required.A freeze occurs between 00:19:21 and 00:19:37
hajimehoshi commentedon Jan 13, 2025
Apparently most of the threads got stuck at
NtWaitForSingleObject
when freezing, but I have no idea what was going on there...Thread 0x4 (

gameoverlayrenderer64!OverlayHookD3D3
seems a hooked function by Steam)Thread 0x5

[-]a Windows application launched via Steam sometimes freezes[/-][+]runtime: a Windows application launched via Steam sometimes freezes[/+]mknyszek commentedon Jan 13, 2025
CC @golang/runtime
qmuntal commentedon Jan 13, 2025
Does this issue reproduce with Go 1.22?
hajimehoshi commentedon Jan 13, 2025
Yes, I could reproduce this with Go 1.22.9.
mknyszek commentedon Jan 13, 2025
Do you have a stack trace for each thread, for example from a debugger?
My guess based on the information provided would be that the threads are stuck in some GC-related thing (spinning up GC mark workers? that only happens on the first GC if
GOMAXPROCS
doesn't ever change) on anote
(https://cs.opensource.google/go/go/+/master:src/runtime/os_windows.go;l=676;drc=4f881115d4067bda8a236aabcae8c41cdd13b4d0). I couldn't tell you why that would be a problem, since this happens all the time.Alternatively, the threads are going to sleep for STW, which is also based on a
note
(https://cs.opensource.google/go/go/+/master:src/runtime/proc.go;l=1612;drc=f025d19e7b3f0c66242760c213cc2b54cb100f69), and perhaps something is preventing the thread stopping the world from continuing promptly. Note that the thread which is stopping-the-world spins, sleeping in 100ms increments (but is awoken early when the last thread goes to sleep). So it might be that too. We could confirm/deny this by disabling the GC but callingruntime.ReadMemStats
at some regular interval. This also happens all the time without issue, though, so I also don't really have a guess as to why this would be a problem running under Steam.hajimehoshi commentedon Jan 13, 2025
I'm afraid I'm not familiar with WinDbg. How can I get the stack traces as a file? I'll try tomorrow.
Also I can insert printlns in the runtime by -overlay. I'll try this later too.
10 remaining items
hajimehoshi commentedon Jan 14, 2025
Hmm, addr2line still didn't resolve the file like this:
hajimehoshi commentedon Jan 14, 2025
I am not sure this would be helpful, but I objdumped the binary and searched the binaries in the suspicous stack traces. I realized the addresses are 'shifted'.
Stack traces with ReleaseMutex (I newly dumped this):
00000000007FE029
:00000000007FC40D
:00000000007FC4C1
:00000000007C1B9B
hajimehoshi commentedon Jan 14, 2025
Here is the resolved symbols in the stack trace below
gameoverlayrenderer64.OverlayHookD3D3+27380
:prattmic commentedon Jan 14, 2025
Apologies for the bad instructions, I don't use addr2line much.
That stack is really interesting. On Windows, asynchronous preemption uses the Windows thread suspend/resume APIs. It seems that this thread is getting stuck in ResumeThread. Maybe the Steam overlay messes with this API somehow?
mknyszek commentedon Jan 14, 2025
@hajimehoshi Can you reproduce with
GODEBUG=asyncpreemptoff=1
?hajimehoshi commentedon Jan 15, 2025
I couldn't reproduce this with
GODEBUG=asyncpreemptoff=1
. I tried 10 times and watched each trial reached 10,000 GCs without any issues.hajimehoshi commentedon Jan 15, 2025
By the way, is there a compile option to specify
asyncpreemptoff=1
by default?//go:debug
didn't recognizeasyncpreemptoff=1
unfortunately. For Steam games, it is not feasible to ask users to set an environment variableGODEBUG
for this, so I'd like to specify this at the compile time. The only way to do this is using-overlay
to rewriteos_windows.go
, but is my understanding correct?Of course, it would be the best that the runtime is fixed to suppress this issue.
There seem multiple situations where
asyncpreemptoff=1
is a workaround.EDIT:
-ldflags="-X=runtime.godebugDefault=asyncpreemptoff=1"
seemed to work as intended.mknyszek commentedon Jan 15, 2025
I think we should fix that, though I'm glad you found another workaround with
-ldflags
. Maybe that makes more sense long-term? This issue seems to be related to the specific DLL injection (?) that Steam does (gameoverlayrenderer64.OverlayHookD3D3+27380
is pretty fishy). It would be unfortunate to disable async preemption for all users of a given package, since it does provide tangible latency benefits.hajimehoshi commentedon Jan 15, 2025
Yeah, so would it be possible for
//go:debug
to acceptasyncpreemptoff=1
? I'm happy to file this if needed. This would be much better and less hacky than-ldflags
. Also, I think this would be very helpful for various situations where this settings is required.I mean, I want to disable async preemption when I build my games for Steam Windows. So I would specify
asyncpreemptoff=1
for a main package (with a build tag). I don't intend to disable it for all the users of my package.prattmic commentedon Jan 15, 2025
I think this is reasonable, please do file an issue.
//go:debug
only has an effect in main packages anyways, so applying to all users of a package isn't a problem.That said, I don't think this is the final resolution. Steam seems to be messing with our process in a way that breaks certain Windows API calls. I think the next step is to report this bug to Valve, as current evidence points to a bug in whatever Steam is doing.
hajimehoshi commentedon Jan 15, 2025
Thanks! #71283
I agree. I've already reported the current situation https://steamcommunity.com/discussions/forum/0/595138100650327297/.
I'll try to make a simple C program that invokes
SuspendThread
andResumeThread
and tries to cause the freeze when I have time.I appreciate all of your quick responses!
mknyszek commentedon Jan 15, 2025
Apologies, the
//go:debug
stuff is new to me. There's more than justGODEBUG
setting that we should allow; perhaps we should file a new issue capturing all of them. EDIT: I'm clearly behind, #71283 already exists. 😅 Thanks!