From 8f89c15e725e8487ded62912ef264ca7a220a76c Mon Sep 17 00:00:00 2001 From: VERSE Date: Thu, 2 Jul 2026 16:53:42 +0700 Subject: [PATCH] perf: prevent memory leak, data race and speed up ComputeCursorCol processing (#15) - fix(wrapper): memory leak and data race (https://github.com/versenilvis/IRIS/commit/4361febfc92858f34ff2a2b77df8298addbefa75) - perf(overlay):speed up ComputeCursorCol processing (https://github.com/versenilvis/IRIS/commit/918b067cfc9f71c4fa52e1573865ab3612acb7d8) --- integration/overlay.go | 5 +++ root/wrapper.go | 77 ++++++++++++++++++++++-------------------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/integration/overlay.go b/integration/overlay.go index f21b39a..d016a5a 100644 --- a/integration/overlay.go +++ b/integration/overlay.go @@ -117,6 +117,11 @@ func ComputeCursorCol(data []byte) int { i++ continue } + if b < 0x7f { + col++ + i++ + continue + } r, size := utf8.DecodeRune(data[i:]) w := lipgloss.Width(string(r)) col += w diff --git a/root/wrapper.go b/root/wrapper.go index 8034eda..5afbf9e 100644 --- a/root/wrapper.go +++ b/root/wrapper.go @@ -82,7 +82,7 @@ func runWrapper() { var naiveBuffer string cursorOffset := 0 var bufferMu sync.Mutex - var userNavigated bool + var userNavigated atomic.Bool var renderMenuNow func() r, w, err := os.Pipe() // pipe for ipc communication from shell to iris @@ -164,8 +164,8 @@ func runWrapper() { } if c.Process != nil { - cwd, err := os.Readlink(fmt.Sprintf("/proc/%d/cwd", c.Process.Pid)) - if err != nil { + cwd, linkErr := os.Readlink(fmt.Sprintf("/proc/%d/cwd", c.Process.Pid)) + if linkErr != nil { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) out, errCmd := exec.CommandContext(ctx, "lsof", "-p", fmt.Sprintf("%d", c.Process.Pid), "-a", "-d", "cwd", "-F", "n").Output() cancel() @@ -173,13 +173,13 @@ func runWrapper() { for _, line := range strings.Split(string(out), "\n") { if strings.HasPrefix(line, "n") { cwd = strings.TrimSpace(line[1:]) - err = nil + linkErr = nil break } } } } - if err == nil { + if linkErr == nil { _ = os.Chdir(cwd) } _ = syscall.Kill(c.Process.Pid, syscall.SIGKILL) @@ -188,9 +188,9 @@ func runWrapper() { restoreTerminal() execArgs := []string{os.Args[0]} - if logDir, err := config.CachePath(); err == nil { + if logDir, pathErr := config.CachePath(); pathErr == nil { argsFile := filepath.Join(logDir, "reload-args") - if data, err := os.ReadFile(argsFile); err == nil { + if data, readErr := os.ReadFile(argsFile); readErr == nil { lines := strings.Split(string(data), "\n") for _, line := range lines { trimmed := strings.TrimSpace(line) @@ -216,6 +216,18 @@ func runWrapper() { pendingUpdate = startBackgroundUpdateCheck() updatePrinted := false + shellPGID, err := unix.Getpgid(core.ShellPID) + if err != nil { + shellPGID = core.ShellPID + } + isExecuting := func() bool { + pgrp, err := unix.IoctlGetInt(int(ptmx.Fd()), unix.TIOCGPGRP) + if err != nil { + return false + } + return pgrp != shellPGID + } + // bridge pty output to actual stdout go func() { defer func() { @@ -242,9 +254,12 @@ func runWrapper() { bufferMu.Lock() nbEmpty := naiveBuffer == "" + navigated := userNavigated.Load() bufferMu.Unlock() - if nbEmpty && !userNavigated { + if isExecuting() { + lastPromptBuf = nil + } else if nbEmpty && !navigated { lastPromptBuf = append(lastPromptBuf, buf[:n]...) if idx := bytes.LastIndexByte(lastPromptBuf, '\n'); idx >= 0 { lastPromptBuf = append([]byte(nil), lastPromptBuf[idx+1:]...) @@ -261,18 +276,6 @@ func runWrapper() { disableGhostText.Store(!config.Get().UI.GhostText) var renderOverlay func() - isExecuting := func() bool { - pgrp, err := unix.IoctlGetInt(int(ptmx.Fd()), unix.TIOCGPGRP) - if err != nil { - return false - } - shellPGID, err := unix.Getpgid(core.ShellPID) - if err != nil { - return pgrp != core.ShellPID - } - return pgrp != shellPGID - } - // listen for suggestion requests from shell scripts via the ipc pipe go func() { defer func() { @@ -376,7 +379,7 @@ func runWrapper() { modeCopy := activeMode activeModeMu.RUnlock() - navCopy := userNavigated + navCopy := userNavigated.Load() runes := []rune(bufCopy) if offsetCopy > 0 && offsetCopy <= len(runes) { @@ -435,7 +438,7 @@ func runWrapper() { return } - if userNavigated { + if userNavigated.Load() { return } @@ -505,7 +508,7 @@ func runWrapper() { if overlay.Visible && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') { intercepted = true - userNavigated = true + userNavigated.Store(true) overlay.UserNavigated = true // clear ghost text synchronously @@ -596,7 +599,7 @@ func runWrapper() { cursorOffset = 0 bufferMu.Unlock() - userNavigated = true + userNavigated.Store(true) overlay.UserNavigated = true writeStdout([]byte(overlay.Render())) @@ -648,7 +651,7 @@ func runWrapper() { cursorOffset = len(naiveBuffer) } shouldOverlayDraw = true - userNavigated = false + userNavigated.Store(false) } bufferMu.Unlock() isLeftRightArrow = true @@ -668,7 +671,7 @@ func runWrapper() { cursorOffset = 0 } shouldOverlayDraw = true - userNavigated = false + userNavigated.Store(false) } bufferMu.Unlock() isLeftRightArrow = true @@ -710,14 +713,14 @@ func runWrapper() { saveMode(activeMode) activeModeMu.Unlock() logger.Debugf("Intercepted Ctrl+R, toggled mode to %q", activeMode) - if userNavigated { + if userNavigated.Load() { bufferMu.Lock() naiveBuffer = overlay.TypedQuery cursorOffset = 0 bufferMu.Unlock() _, _ = ptmx.Write(append([]byte{0x15}, overlay.TypedQuery...)) } - userNavigated = false + userNavigated.Store(false) overlay.UserNavigated = false shouldOverlayDraw = true // enter: enter behavior is a bit different from tab suggestions in code editor @@ -746,7 +749,7 @@ func runWrapper() { bufferMu.Unlock() disableGhostText.Store(false) shouldOverlayDraw = false - userNavigated = false + userNavigated.Store(false) continue } else if b == 0x09 { // tab: select suggestions intercepted = true @@ -776,7 +779,7 @@ func runWrapper() { shouldOverlayDraw = true // <- rerender after tab to choose, if you set to false, // when you press tab continually, it will print all folder from menu suggestions // and make the cursor jump to next line - userNavigated = false + userNavigated.Store(false) } continue } @@ -793,7 +796,7 @@ func runWrapper() { shouldOverlayDraw = true } bufferMu.Unlock() - userNavigated = false + userNavigated.Store(false) case 0x05: // ctrl+e: move to end of line bufferMu.Lock() cursorOffset = 0 @@ -801,7 +804,7 @@ func runWrapper() { shouldOverlayDraw = true } bufferMu.Unlock() - userNavigated = false + userNavigated.Store(false) case 127, 0x08: // backspace: remove character bufferMu.Lock() @@ -824,7 +827,7 @@ func runWrapper() { } bufferMu.Unlock() shouldOverlayDraw = true - userNavigated = false + userNavigated.Store(false) case 0x17: // ctrl+w: delete the last word in the buffer bufferMu.Lock() trimBuf := strings.TrimRight(naiveBuffer, " ") @@ -837,10 +840,10 @@ func runWrapper() { cursorOffset = 0 bufferMu.Unlock() shouldOverlayDraw = true - userNavigated = false + userNavigated.Store(false) case 0x0c: // ctrl+l: clear screen but keep buffer and redraw menu shouldOverlayDraw = true - userNavigated = false + userNavigated.Store(false) case '\r', '\n', 0x03, 0x15: // enter, ctrl+c, ctrl+u: clear buffer on line reset bufferMu.Lock() naiveBuffer = "" @@ -851,7 +854,7 @@ func runWrapper() { activeModeMu.Unlock() disableGhostText.Store(false) writeStdout([]byte(overlay.ClearAndDisable())) - userNavigated = false + userNavigated.Store(false) default: // track normal printable characters in the buffer for matching if b >= 32 && b <= 126 { @@ -892,7 +895,7 @@ func runWrapper() { } bufferMu.Unlock() shouldOverlayDraw = true - userNavigated = false + userNavigated.Store(false) overlay.UserNavigated = false } }