Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neovim freezes on smooth scroll #510

Open
quolpr opened this issue Oct 24, 2024 · 2 comments
Open

Neovim freezes on smooth scroll #510

quolpr opened this issue Oct 24, 2024 · 2 comments
Labels
bug Something isn't working

Comments

@quolpr
Copy link

quolpr commented Oct 24, 2024

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 quolpr added the bug Something isn't working label Oct 24, 2024
@lewis6991
Copy link
Member

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
Copy link
Author

quolpr commented Oct 24, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants