nvim-treesitter / nvim-treesitter-context

Show code context
MIT License
2.5k stars 201 forks source link

Neovim freezes on smooth scroll #510

Open quolpr opened 1 week ago

quolpr commented 1 week ago

Description

When I scroll this swift file with 387 LOC:

Click me ```swift import Foundation import AVFoundation import CoreGraphics import VideoToolbox import AppKit // MARK: - Virtual Desktop Handler class VirtualDesktopManager { private var displayStream: CGDisplayStream? private let queue = DispatchQueue(label: "com.videostreaming.capture") func startCapturing(width: Int, height: Int, handler: @escaping (CGImage?) -> Void) { print("Checking screen recording permissions...") // Check screen recording permission if CGPreflightScreenCaptureAccess() { print("Screen recording permission already granted") } else { print("Requesting screen recording permission...") CGRequestScreenCaptureAccess() // Wait for permission while !CGPreflightScreenCaptureAccess() { Thread.sleep(forTimeInterval: 0.1) } print("Screen recording permission granted") } print("Starting screen capture...") let displayID = CGMainDisplayID() // Get the display bounds let displayWidth = CGDisplayPixelsWide(displayID) let displayHeight = CGDisplayPixelsHigh(displayID) // Calculate scaled dimensions while maintaining aspect ratio let scale = min(Double(width) / Double(displayWidth), Double(height) / Double(displayHeight)) let scaledWidth = Int(Double(displayWidth) * scale) let scaledHeight = Int(Double(displayHeight) * scale) print("Display dimensions: \(displayWidth)x\(displayHeight)") print("Scaled dimensions: \(scaledWidth)x\(scaledHeight)") let properties: [CFString: Any] = [ CGDisplayStream.showCursor: true, CGDisplayStream.minimumFrameTime: 1.0/30.0 ] displayStream = CGDisplayStream( dispatchQueueDisplay: displayID, outputWidth: scaledWidth, outputHeight: scaledHeight, pixelFormat: Int32(kCVPixelFormatType_32BGRA), properties: properties as CFDictionary, queue: queue, handler: { [weak self] (status, displayTime, frameSurface, error) in guard let self = self else { return } switch status { case .frameComplete: if let frameSurface = frameSurface, let image = self.createCGImage(from: frameSurface) { handler(image) } case .stopped: print("Display stream stopped") case .frameBlank: print("Frame blank") case .frameIdle: print("Frame idle") @unknown default: print("Unknown frame status: \(status)") } } ) if displayStream == nil { print("Failed to create display stream") return } print("Starting display stream...") if let startError = displayStream?.start() { print("Display stream start failed with error: \(startError)") // Print error code print("Error code: \(startError.rawValue)") // Handle common error cases switch startError.rawValue { case 1000: print("Permission denied or not available") case 1001: print("Invalid display") case 1002: print("Invalid parameters") default: print("Unknown error") } } else { print("Display stream started successfully") } } private func createCGImage(from surface: IOSurfaceRef) -> CGImage? { let width = IOSurfaceGetWidth(surface) let height = IOSurfaceGetHeight(surface) let bytesPerRow = IOSurfaceGetBytesPerRow(surface) let surfaceData = IOSurfaceGetBaseAddress(surface) guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else { return nil } let context = CGContext( data: surfaceData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue ) return context?.makeImage() } func stopCapturing() { print("Stopping screen capture...") displayStream?.stop() displayStream = nil } } // MARK: - Video Streaming Manager class VideoStreamingManager { private let desktopManager = VirtualDesktopManager() private let encoder: VideoEncoder private let streamServer = StreamServer() private var frameCount: Int64 = 0 private let width: Int32 private let height: Int32 private let clientAddress: String private let clientPort: UInt16 init(width: Int32, height: Int32, clientAddress: String, clientPort: UInt16) { self.width = width self.height = height self.clientAddress = clientAddress self.clientPort = clientPort self.encoder = VideoEncoder(width: width, height: height) print("VideoStreamingManager initialized") } func startCapture() { print("Starting video capture and streaming...") do { try streamServer.startServer(port: 12345) streamServer.setClient(address: clientAddress, port: clientPort) print("UDP server started on port 12345, sending to \(clientAddress):\(clientPort)") desktopManager.startCapturing(width: Int(width), height: Int(height)) { [weak self] cgImage in guard let self = self, let image = cgImage else { return } let timestamp = CMTime(value: self.frameCount, timescale: 30) self.frameCount += 1 if self.frameCount % 30 == 0 { print("Processed \(self.frameCount) frames") } self.encoder.encode(image: image, presentationTimeStamp: timestamp) { encodedData in if let data = encodedData { do { var header = PacketHeader( frameNumber: UInt32(self.frameCount), timestamp: UInt64(timestamp.value), payloadSize: UInt32(data.count) ) var packetData = Data(bytes: &header, count: MemoryLayout.size) packetData.append(data) try self.streamServer.send(data: packetData) } catch { print("Error sending frame \(self.frameCount): \(error)") } } } } } catch { print("Error starting capture: \(error)") } } func stopCapture() { print("Stopping video capture and streaming...") desktopManager.stopCapturing() streamServer.closeConnection() } deinit { stopCapture() } } // MARK: - Video Encoder class VideoEncoder { private var session: VTCompressionSession? private let width: Int32 private let height: Int32 private let fps: Int32 init(width: Int32, height: Int32, fps: Int32 = 30) { self.width = width self.height = height self.fps = fps setupSession() } private func setupSession() { var session: VTCompressionSession? let status = VTCompressionSessionCreate( allocator: kCFAllocatorDefault, width: width, height: height, codecType: kCMVideoCodecType_H264, encoderSpecification: nil, imageBufferAttributes: nil, compressedDataAllocator: nil, outputCallback: nil, refcon: nil, compressionSessionOut: &session ) guard status == noErr, let session = session else { return } VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue) VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Main_AutoLevel) VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: NSNumber(value: 2000000)) VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: fps)) VTCompressionSessionPrepareToEncodeFrames(session) self.session = session } func encode(image: CGImage, presentationTimeStamp: CMTime, completion: @escaping (Data?) -> Void) { guard let session = session else { return } var pixelBuffer: CVPixelBuffer? let status = CVPixelBufferCreate( kCFAllocatorDefault, image.width, image.height, kCVPixelFormatType_32BGRA, nil, &pixelBuffer ) guard status == kCVReturnSuccess, let pixelBuffer = pixelBuffer else { return } CVPixelBufferLockBaseAddress(pixelBuffer, []) let context = CGContext( data: CVPixelBufferGetBaseAddress(pixelBuffer), width: image.width, height: image.height, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue ) context?.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) CVPixelBufferUnlockBaseAddress(pixelBuffer, []) var flags: VTEncodeInfoFlags = [] VTCompressionSessionEncodeFrame( session, imageBuffer: pixelBuffer, presentationTimeStamp: presentationTimeStamp, duration: CMTime.invalid, frameProperties: nil, infoFlagsOut: &flags, outputHandler: { status, flags, sampleBuffer in guard let sampleBuffer = sampleBuffer else { return } if CMSampleBufferDataIsReady(sampleBuffer) { if let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) { var length = 0 var dataPointer: UnsafeMutablePointer? CMBlockBufferGetDataPointer( dataBuffer, atOffset: 0, lengthAtOffsetOut: nil, totalLengthOut: &length, dataPointerOut: &dataPointer ) if let pointer = dataPointer { let data = Data(bytes: pointer, count: length) completion(data) } } } } ) } } // MARK: - UDP Stream Server class StreamServer { private var socket: Int32 = -1 private var clientAddr: sockaddr_in? func startServer(port: UInt16) throws { socket = Darwin.socket(AF_INET, SOCK_DGRAM, 0) guard socket >= 0 else { throw NSError(domain: "Socket creation failed", code: -1) } var addr = sockaddr_in() addr.sin_family = sa_family_t(AF_INET) addr.sin_port = port.bigEndian addr.sin_addr.s_addr = INADDR_ANY.littleEndian let bindResult = withUnsafePointer(to: &addr) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in bind(socket, sockPtr, socklen_t(MemoryLayout.stride)) } } guard bindResult == 0 else { throw NSError(domain: "Bind failed", code: -2) } var bufferSize = Int32(65535 * 10) setsockopt(socket, SOL_SOCKET, SO_SNDBUF, &bufferSize, socklen_t(MemoryLayout.size)) } func setClient(address: String, port: UInt16) { var addr = sockaddr_in() addr.sin_family = sa_family_t(AF_INET) addr.sin_port = port.bigEndian addr.sin_addr.s_addr = inet_addr(address.cString(using: .utf8)) clientAddr = addr } func send(data: Data) throws { guard let clientAddr = clientAddr else { return } let maxChunkSize = 65507 var offset = 0 while offset < data.count { let chunkSize = min(maxChunkSize, data.count - offset) let chunk = data.subdata(in: offset..<(offset + chunkSize)) let sendResult = withUnsafePointer(to: clientAddr) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in chunk.withUnsafeBytes { buffer in sendto(socket, buffer.baseAddress, chunk.count, 0, sockPtr, socklen_t(MemoryLayout.stride)) } } } if sendResult < 0 { throw NSError(domain: "Send failed", code: -3) } offset += chunkSize } } func closeConnection() { if socket >= 0 { close(socket) } } } // MARK: - Packet Header Structure struct PacketHeader { var frameNumber: UInt32 var timestamp: UInt64 var payloadSize: UInt32 } ```

It's freezes on scroll. I debugged it with stevearc/profile.nvim, and here what I got: Image And here is without context: Image

So the problem is coming from vim.treesitter.query.parse. You can analyze by yourself it, here is traces:

With nvim-treesitter-context: https://drive.google.com/file/d/1nv_GdnRguZFtjgqA0R_hWYiX0VkiKhMn/view?usp=drive_link Without: https://drive.google.com/file/d/1MYBKxS_SE4vHTzhwG9C4mD4TYJjo6kt8/view?usp=drive_link

You can load them at https://ui.perfetto.dev/

As one of maybe stupid suggestion - could we cache query parse if file not changed? But I absolutely don't have idea how this parse works.

Neovim version

NVIM v0.10.1

Expected behavior

Scroll should be smooth

Actual behavior

Scroll freezes nvim

Minimal config

local plugins = {
    ts = "https://github.com/nvim-treesitter/nvim-treesitter",
    ts_context = "https://github.com/nvim-treesitter/nvim-treesitter-context",
    -- ADD ADDITIONAL PLUGINS THAT ARE _NECESSARY_ TO REPRODUCE THE ISSUE
}

for name, url in pairs(plugins) do
    local install_path = "/tmp/nvim/site/" .. name
    if vim.fn.isdirectory(install_path) == 0 then
        vim.fn.system({ "git", "clone", "--depth=1", url, install_path })
    end
    vim.o.runtimepath = install_path .. "," .. vim.o.runtimepath
end

require("nvim-treesitter.configs").setup({
    ensure_installed = { "swift" },
    -- Autoinstall languages that are not installed
    auto_install = true,
    highlight = { enable = true },
    indent = { enable = true },
})

-- ADD INIT.LUA SETTINGS THAT IS _NECESSARY_ FOR REPRODUCING THE ISSUE
require("treesitter-context").setup({
    enable = true,
    max_lines = 10,
})

Steps to reproduce

  1. nvim --clean -u minimal.lua
  2. Open swift file
  3. Make smooth scroll(with trackpad or mouse)
lewis6991 commented 1 week ago

As one of maybe stupid suggestion - could we cache query parse if file not changed? But I absolutely don't have idea how this parse works.

It already is: https://github.com/neovim/neovim/blob/master/runtime/lua/vim/treesitter/query.lua#L216

However, the cache is invalidated on garbage collection, so one cause is that your system has too much memory pressure.

quolpr commented 1 week ago

@lewis6991 hmm, weird. As for memory pressure, here is a screenshot of free mem: https://github.com/user-attachments/assets/c9717223-3b50-4c46-8f0a-0064992804a7 . So only 50% of mem used, no high CPU/MEM load. And I have 36GB of ram in total

Also, another interesting observation - that doesn't happen for go files, for example.