Skip to content

Commit 46a2569

Browse files
claudeJanDeDobbeleer
authored andcommitted
perf(battery): use GetSystemPowerStatus on Windows instead of SetupAPI/IOCTL
The previous implementation enumerated battery devices via SetupAPI and issued three sequential DeviceIoControl calls (IOCTL_BATTERY_QUERY_TAG, _INFORMATION, _STATUS) per battery. The STATUS query round-trips to the embedded controller, which is the dominant cost users reported (~80ms per prompt render, see #1599). GetSystemPowerStatus reads a value Windows already maintains for the taskbar battery icon, so it returns percentage and charging state without hitting the EC. The fields it can't provide (Voltage, Full/ Current capacity in mWh) aren't exposed by battery.Info today, so there's no loss of currently displayed data.
1 parent 7dd5e8b commit 46a2569

1 file changed

Lines changed: 43 additions & 287 deletions

File tree

src/runtime/battery/battery_windows.go

Lines changed: 43 additions & 287 deletions
Original file line numberDiff line numberDiff line change
@@ -29,310 +29,66 @@ import (
2929
"golang.org/x/sys/windows"
3030
)
3131

32-
type batteryQueryInformation struct {
33-
BatteryTag uint32
34-
InformationLevel int32
35-
AtRate int32
32+
// systemPowerStatus mirrors the Win32 SYSTEM_POWER_STATUS struct. GetSystemPowerStatus
33+
// reads a value the OS already maintains, so unlike the battery miniclass IOCTLs
34+
// (IOCTL_BATTERY_QUERY_TAG/INFORMATION/STATUS) it never round-trips to the embedded
35+
// controller, which is what made the previous SetupAPI-based implementation slow.
36+
type systemPowerStatus struct {
37+
ACLineStatus byte
38+
BatteryFlag byte
39+
BatteryLifePercent byte
40+
SystemStatusFlag byte
41+
BatteryLifeTime uint32
42+
BatteryFullLifeTime uint32
3643
}
3744

38-
type batteryInformation struct {
39-
Capabilities uint32
40-
Technology uint8
41-
Reserved [3]uint8
42-
Chemistry [4]uint8
43-
DesignedCapacity uint32
44-
FullChargedCapacity uint32
45-
DefaultAlert1 uint32
46-
DefaultAlert2 uint32
47-
CriticalBias uint32
48-
CycleCount uint32
49-
}
50-
51-
type batteryWaitStatus struct {
52-
BatteryTag uint32
53-
Timeout uint32
54-
PowerState uint32
55-
LowCapacity uint32
56-
HighCapacity uint32
57-
}
58-
59-
type batteryStatus struct {
60-
PowerState uint32
61-
Capacity uint32
62-
Voltage uint32
63-
Rate int32
64-
}
45+
const (
46+
batteryFlagCharging = 0x08
47+
batteryFlagCritical = 0x04
48+
batteryFlagNoBattery = 0x80
49+
batteryFlagUnknown = 0xFF
6550

66-
type guid struct {
67-
Data1 uint32
68-
Data2 uint16
69-
Data3 uint16
70-
Data4 [8]byte
71-
}
72-
73-
type spDeviceInterfaceData struct {
74-
cbSize uint32
75-
InterfaceClassGuid guid
76-
Flags uint32
77-
Reserved uint
78-
}
51+
batteryLifePercentUnknown = 0xFF
7952

80-
var guidDeviceBattery = guid{
81-
0x72631e54,
82-
0x78A4,
83-
0x11d0,
84-
[8]byte{0xbc, 0xf7, 0x00, 0xaa, 0x00, 0xb7, 0xb3, 0x2a},
85-
}
53+
acLineOnline = 1
54+
)
8655

87-
func uint32ToFloat64(num uint32) (float64, error) {
88-
if num == 0xffffffff { // BATTERY_UNKNOWN_CAPACITY
89-
return 0, errors.New("unknown value received")
90-
}
91-
return float64(num), nil
92-
}
56+
var kernel32 = &windows.LazyDLL{Name: "kernel32.dll", System: true}
57+
var getSystemPowerStatus = kernel32.NewProc("GetSystemPowerStatus")
9358

94-
func setupDiSetup(proc *windows.LazyProc, args ...uintptr) (uintptr, error) {
95-
r1, _, errno := syscall.SyscallN(proc.Addr(), args...)
96-
if windows.Handle(r1) == windows.InvalidHandle {
97-
if errno != 0 {
98-
return 0, error(errno)
99-
}
100-
return 0, syscall.EINVAL
101-
}
102-
return r1, nil
103-
}
59+
func systemGetAll() ([]*battery, error) {
60+
var sps systemPowerStatus
10461

105-
func setupDiCall(proc *windows.LazyProc, args ...uintptr) syscall.Errno {
106-
r1, _, errno := syscall.SyscallN(proc.Addr(), args...)
62+
r1, _, errno := syscall.SyscallN(getSystemPowerStatus.Addr(), uintptr(unsafe.Pointer(&sps)))
10763
if r1 == 0 {
10864
if errno != 0 {
109-
return errno
65+
return nil, error(errno)
11066
}
111-
return syscall.EINVAL
67+
return nil, syscall.EINVAL
11268
}
113-
return 0
114-
}
11569

116-
var setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true}
117-
var setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW")
118-
var setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces")
119-
var setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
120-
var setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList")
121-
122-
func readState(powerState uint32) State {
123-
switch {
124-
case powerState&0x00000004 != 0:
125-
return Charging
126-
case powerState&0x00000008 != 0:
127-
return Empty
128-
case powerState&0x00000002 != 0:
129-
return Discharging
130-
case powerState&0x00000001 != 0:
131-
return Full
132-
default:
133-
return Unknown
134-
}
135-
}
136-
137-
func systemGet(idx int) (*battery, error) {
138-
hdev, err := setupDiSetup(
139-
setupDiGetClassDevsW,
140-
uintptr(unsafe.Pointer(&guidDeviceBattery)),
141-
0,
142-
0,
143-
2|16, // DIGCF_PRESENT|DIGCF_DEVICEINTERFACE
144-
)
145-
146-
if err != nil {
147-
return nil, err
148-
}
149-
150-
defer func() {
151-
_, _, _ = syscall.SyscallN(setupDiDestroyDeviceInfoList.Addr(), hdev)
152-
}()
153-
154-
var did spDeviceInterfaceData
155-
did.cbSize = uint32(unsafe.Sizeof(did))
156-
errno := setupDiCall(
157-
setupDiEnumDeviceInterfaces,
158-
hdev,
159-
0,
160-
uintptr(unsafe.Pointer(&guidDeviceBattery)),
161-
uintptr(idx),
162-
uintptr(unsafe.Pointer(&did)),
163-
)
164-
165-
if errno == 259 { // ERROR_NO_MORE_ITEMS
166-
return nil, ErrNotFound
167-
}
168-
169-
if errno != 0 {
170-
return nil, errno
171-
}
172-
173-
var cbRequired uint32
174-
errno = setupDiCall(
175-
setupDiGetDeviceInterfaceDetailW,
176-
hdev,
177-
uintptr(unsafe.Pointer(&did)),
178-
0,
179-
0,
180-
uintptr(unsafe.Pointer(&cbRequired)),
181-
0,
182-
)
183-
184-
if errno != 0 && errno != 122 { // ERROR_INSUFFICIENT_BUFFER
185-
return nil, errno
186-
}
187-
188-
if cbRequired == 0 {
189-
return nil, errors.New("no buffer information returned")
190-
}
191-
192-
// The god damn struct with ANYSIZE_ARRAY of utf16 in it is crazy.
193-
// So... let's emulate it with array of uint16 ;-D.
194-
// Keep in mind that the first two elements are actually cbSize.
195-
didd := make([]uint16, cbRequired/2)
196-
cbSize := (*uint32)(unsafe.Pointer(&didd[0]))
197-
if unsafe.Sizeof(uint(0)) == 8 {
198-
*cbSize = 8
199-
} else {
200-
*cbSize = 6
201-
}
202-
203-
errno = setupDiCall(
204-
setupDiGetDeviceInterfaceDetailW,
205-
hdev,
206-
uintptr(unsafe.Pointer(&did)),
207-
uintptr(unsafe.Pointer(&didd[0])),
208-
uintptr(cbRequired),
209-
uintptr(unsafe.Pointer(&cbRequired)),
210-
0,
211-
)
212-
213-
if errno != 0 {
214-
return nil, errno
215-
}
216-
217-
devicePath := &didd[2:][0]
218-
219-
handle, err := windows.CreateFile(
220-
devicePath,
221-
windows.GENERIC_READ|windows.GENERIC_WRITE,
222-
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
223-
nil,
224-
windows.OPEN_EXISTING,
225-
windows.FILE_ATTRIBUTE_NORMAL,
226-
0,
227-
)
228-
229-
if err != nil {
230-
return nil, err
231-
}
232-
233-
defer func() {
234-
_ = windows.CloseHandle(handle)
235-
}()
236-
237-
var dwOut uint32
238-
239-
var dwWait uint32
240-
var bqi batteryQueryInformation
241-
err = windows.DeviceIoControl(
242-
handle,
243-
2703424, // IOCTL_BATTERY_QUERY_TAG
244-
(*byte)(unsafe.Pointer(&dwWait)),
245-
uint32(unsafe.Sizeof(dwWait)),
246-
(*byte)(unsafe.Pointer(&bqi.BatteryTag)),
247-
uint32(unsafe.Sizeof(bqi.BatteryTag)),
248-
&dwOut,
249-
nil,
250-
)
251-
252-
if err != nil {
253-
return nil, err
254-
}
255-
256-
if bqi.BatteryTag == 0 {
257-
return nil, errors.New("battery tag not returned")
258-
}
259-
260-
b := &battery{}
261-
262-
var bi batteryInformation
263-
err = windows.DeviceIoControl(
264-
handle,
265-
2703428, // IOCTL_BATTERY_QUERY_INFORMATION
266-
(*byte)(unsafe.Pointer(&bqi)),
267-
uint32(unsafe.Sizeof(bqi)),
268-
(*byte)(unsafe.Pointer(&bi)),
269-
uint32(unsafe.Sizeof(bi)),
270-
&dwOut,
271-
nil,
272-
)
273-
274-
if err != nil {
275-
return nil, err
276-
}
277-
278-
b.Full = float64(bi.FullChargedCapacity)
279-
280-
bws := batteryWaitStatus{BatteryTag: bqi.BatteryTag}
281-
282-
var bs batteryStatus
283-
err = windows.DeviceIoControl(
284-
handle,
285-
2703436, // IOCTL_BATTERY_QUERY_STATUS
286-
(*byte)(unsafe.Pointer(&bws)),
287-
uint32(unsafe.Sizeof(bws)),
288-
(*byte)(unsafe.Pointer(&bs)),
289-
uint32(unsafe.Sizeof(bs)),
290-
&dwOut,
291-
nil,
292-
)
293-
294-
if err != nil {
295-
return nil, err
296-
}
297-
298-
if b.Current, err = uint32ToFloat64(bs.Capacity); err != nil {
299-
return nil, err
70+
if sps.BatteryFlag&batteryFlagNoBattery != 0 || sps.BatteryFlag == batteryFlagUnknown {
71+
return nil, &NoBatteryError{}
30072
}
30173

302-
if b.Voltage, err = uint32ToFloat64(bs.Voltage); err != nil {
303-
return nil, err
74+
if sps.BatteryLifePercent == batteryLifePercentUnknown {
75+
return nil, errors.New("unknown value received")
30476
}
30577

306-
b.Voltage /= 1000
307-
b.State = readState(bs.PowerState)
308-
309-
return b, nil
310-
}
311-
312-
func systemGetAll() ([]*battery, error) {
313-
var batteries []*battery
314-
var i int
315-
var errs Errors
78+
b := &battery{Full: 100, Current: float64(min(sps.BatteryLifePercent, 100))}
31679

317-
for i = 0; ; i++ {
318-
b, err := systemGet(i)
319-
if err == ErrNotFound {
320-
break
321-
}
322-
if err != nil {
323-
errs = append(errs, err)
324-
continue
325-
}
326-
batteries = append(batteries, b)
327-
}
328-
329-
if i == 0 {
330-
return nil, &NoBatteryError{}
331-
}
332-
333-
if len(batteries) == 0 {
334-
return nil, errs
80+
switch {
81+
case sps.BatteryFlag&batteryFlagCharging != 0:
82+
b.State = Charging
83+
case sps.BatteryLifePercent == 100 && sps.ACLineStatus == acLineOnline:
84+
b.State = Full
85+
case sps.ACLineStatus == acLineOnline:
86+
b.State = NotCharging
87+
case sps.BatteryFlag&batteryFlagCritical != 0:
88+
b.State = Empty
89+
default:
90+
b.State = Discharging
33591
}
33692

337-
return batteries, nil
93+
return []*battery{b}, nil
33894
}

0 commit comments

Comments
 (0)