Skip to content

Commit 3884879

Browse files
perf: single xdotool call + smoothstep easing for smooth drag
- Replace per-step Go-side sleeps (O(n) fork+exec) with a single xdotool arg chain using inline sleep directives (1 fork+exec) - Add smoothstep easing (3t²-2t³) for step delays: slow at pickup and placement, fast in the middle, matching natural drag behavior - Aligns with the performance-first design in the humanize plan Made-with: Cursor
1 parent f12f833 commit 3884879

1 file changed

Lines changed: 37 additions & 18 deletions

File tree

server/cmd/api/api/computer.go

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,44 +1012,63 @@ func (s *ApiService) doDragMouseSmooth(ctx context.Context, log *slog.Logger, bo
10121012

10131013
result := mousetrajectory.GenerateMultiSegmentTrajectory(waypoints, screenWidth, screenHeight, body.DurationMs)
10141014
points := result.Points
1015-
stepDelayMs := result.StepDelayMs
1015+
baseDelayMs := result.StepDelayMs
10161016

10171017
if len(points) < 2 {
10181018
return nil
10191019
}
10201020

1021-
for i := 1; i < len(points); i++ {
1022-
select {
1023-
case <-ctx.Done():
1024-
return &executionError{msg: "drag movement cancelled"}
1025-
default:
1026-
}
1021+
numSteps := len(points) - 1
10271022

1023+
// Build a single xdotool arg slice with inline sleep directives.
1024+
// Use smoothstep easing: slow at start (pickup) and end (placement),
1025+
// fast in the middle, matching natural human drag behavior.
1026+
args := []string{}
1027+
for i := 1; i <= numSteps; i++ {
10281028
dx := points[i][0] - points[i-1][0]
10291029
dy := points[i][1] - points[i-1][1]
1030-
if dx != 0 || dy != 0 {
1031-
args := []string{"mousemove_relative", "--", strconv.Itoa(dx), strconv.Itoa(dy)}
1032-
if output, err := defaultXdoTool.Run(ctx, args...); err != nil {
1033-
log.Error("xdotool mousemove_relative failed", "err", err, "output", string(output), "step", i)
1034-
return &executionError{msg: "failed during smooth drag movement"}
1035-
}
1030+
if dx == 0 && dy == 0 {
1031+
continue
10361032
}
1037-
jitter := stepDelayMs
1038-
if stepDelayMs > 3 {
1039-
jitter = stepDelayMs + rand.Intn(5) - 2
1033+
args = append(args, "mousemove_relative", "--", strconv.Itoa(dx), strconv.Itoa(dy))
1034+
1035+
if i < numSteps {
1036+
delay := smoothStepDelay(i, numSteps, baseDelayMs*2, baseDelayMs/2)
1037+
jitter := delay + rand.Intn(5) - 2
10401038
if jitter < 3 {
10411039
jitter = 3
10421040
}
1041+
args = append(args, "sleep", fmt.Sprintf("%.3f", float64(jitter)/1000.0))
10431042
}
1044-
if err := sleepWithContext(ctx, time.Duration(jitter)*time.Millisecond); err != nil {
1045-
return &executionError{msg: "drag movement cancelled"}
1043+
}
1044+
1045+
if len(args) > 0 {
1046+
log.Info("executing xdotool (smooth drag move)", "steps", numSteps, "segments", len(body.Path)-1)
1047+
if output, err := defaultXdoTool.Run(ctx, args...); err != nil {
1048+
log.Error("xdotool smooth drag move failed", "err", err, "output", string(output))
1049+
return &executionError{msg: "failed during smooth drag movement"}
10461050
}
10471051
}
10481052

10491053
log.Info("executed smooth drag movement", "points", len(points), "segments", len(body.Path)-1)
10501054
return nil
10511055
}
10521056

1057+
// smoothStepDelay maps position i/n through a smoothstep curve to produce
1058+
// a delay in [fastMs, slowMs]. Slow at start and end, fast in the middle.
1059+
// smoothstep(t) = 3t² - 2t³
1060+
func smoothStepDelay(i, n, slowMs, fastMs int) int {
1061+
if n <= 1 {
1062+
return slowMs
1063+
}
1064+
t := float64(i) / float64(n)
1065+
// Remap t so that 0 and 1 map to 1 (slow) and 0.5 maps to 0 (fast).
1066+
// Use distance from center: d = |2t - 1|, then smoothstep on d.
1067+
d := math.Abs(2*t - 1)
1068+
s := d * d * (3 - 2*d) // smoothstep
1069+
return fastMs + int(float64(slowMs-fastMs)*s)
1070+
}
1071+
10531072
func (s *ApiService) DragMouse(ctx context.Context, request oapi.DragMouseRequestObject) (oapi.DragMouseResponseObject, error) {
10541073
s.inputMu.Lock()
10551074
defer s.inputMu.Unlock()

0 commit comments

Comments
 (0)