nvim-treesitter-context icon indicating copy to clipboard operation
nvim-treesitter-context copied to clipboard

Neovim freezes on smooth scroll

Open quolpr opened this issue 1 year ago • 3 comments

Description

When I scroll this swift file with 387 LOC:

Click me
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<PacketHeader>.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<Int8>?
                        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<sockaddr_in>.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<Int32>.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<sockaddr_in>.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)

quolpr avatar Oct 24 '24 09:10 quolpr

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.

lewis6991 avatar Oct 24 '24 09:10 lewis6991

@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.

quolpr avatar Oct 24 '24 09:10 quolpr

I get freezes with this plugin on go files, as well.

daniel-odrinski avatar Mar 13 '25 12:03 daniel-odrinski