Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 29, 2025

Description

Prettier can hang indefinitely when formatting malformed JSX (e.g., improperly self-closed tags), causing 100% CPU usage and "infinite saving" state with no way to cancel. This PR adds timeout protection and cancellation token support to prevent these infinite hangs.

Changes

Timeout Protection

  • Added prettier.timeout setting (default: 30s, set to 0 to disable)
  • Implemented withTimeout() utility with proper cleanup via try-finally to prevent memory leaks
  • Applied timeout to both PrettierMainThreadInstance (Prettier v2) and PrettierWorkerInstance (Prettier v3+)
  • Timeout is configurable per workspace or globally

Cancellation Support

  • Threaded CancellationToken through formatting pipeline from VS Code's formatter API
  • Added cancellation checks before/after Prettier execution in PrettierEditService
  • Users can abort formatting via Escape key or document close
  • Proper logging when operations are cancelled

Implementation Details

// src/utils/timeout.ts - Memory-safe timeout wrapper
export async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  if (ms <= 0) return promise;
  
  let timeoutId: NodeJS.Timeout | undefined;
  const timeoutPromise = new Promise<never>((_, reject) => {
    timeoutId = setTimeout(() => reject(new Error(`...timed out after ${ms}ms`)), ms);
  });
  
  try {
    return await Promise.race([promise, timeoutPromise]);
  } finally {
    // Clean up timeout to prevent memory leaks
    if (timeoutId !== undefined) clearTimeout(timeoutId);
  }
}

Files Modified

  • package.json - Added timeout configuration setting
  • package.nls.json - Added localization for timeout setting
  • src/types.d.ts - Added timeout to configuration types
  • src/utils/timeout.ts - New timeout utility with proper cleanup
  • src/PrettierInstance.ts - Added setTimeoutMs method to interface
  • src/PrettierMainThreadInstance.ts - Applied timeout to format method
  • src/PrettierWorkerInstance.ts - Applied timeout to format method
  • src/ModuleResolver.ts - Set timeout from config when creating instances
  • src/PrettierEditProvider.ts - Pass cancellation token through pipeline
  • src/PrettierEditService.ts - Check cancellation at strategic points
  • src/test/suite/timeout.test.ts - New comprehensive timeout tests

Related Issue

Fixes #3217

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/Build changes

Checklist

  • I have read the CONTRIBUTING guidelines
  • My code follows the code style of this project (npm run lint passes)
  • I have run npm run prettier to format my code
  • I have added/updated tests that prove my fix or feature works
  • All new and existing tests pass (npm test)
  • I have updated the CHANGELOG.md with a summary of my changes
Original prompt

This section details on the original issue you should resolve

<issue_title>prettier extremly load my cpu and show infinity saving file with 'prettier code formatter'</issue_title>
<issue_description>You asked me about some additionl info - I got this here it is

"use client"

import Image from "next/image"
import { useState } from "react"
import { Carousel } from "react-responsive-carousel"
import "react-responsive-carousel/lib/styles/carousel.min.css"

import { AiFillCaretRight, AiFillCaretLeft } from "react-icons/ai"
import { twMerge } from "tailwind-merge"

interface SliderProps {
  images: string[]
  title: string
  className?: string
  containerClassName?: string
}

export function Slider({ images, title, className = "", containerClassName = "" }: SliderProps) {
  const [currentIndex, setCurrentIndex] = useState(0)
  const [isMoving, setIsMoving] = useState(false)
  const [direction, setDirection] = useState("")

  const goToPreviousSlide = () => {
    if (!isMoving) {
      setIsMoving(true)
      setDirection("left")
      setTimeout(() => {
        setCurrentIndex(prev => (prev === 0 ? images.length - 1 : prev - 1))
        setIsMoving(false)
      }, 300)
    }
  }

  const goToNextSlide = () => {
    if (!isMoving) {
      setIsMoving(true)
      setDirection("right")
      setTimeout(() => {
        setCurrentIndex(prev => (prev === images.length - 1 ? 0 : prev + 1))
        setIsMoving(false)
      }, 300)
    }
  }

  return (
    <figure className={twMerge(`relative w-full laptop:w-fit ${containerClassName}`)}>
      <Carousel
        showArrows={true}
        showIndicators={false}
        showStatus={false}
        showThumbs={false}
        width={480}
        ariaLabel="ariaLabelTest"
        axis="horizontal"
        emulateTouch={true}
        dynamicHeight={true}
        renderArrowNext={() => (
          <button className="absolute z-[88] top-0 bottom-0 left-0 w-[40px] bg-[rgba(0,0,0,0.4)] flex justify-center items-center"/>
        )}>
        {images.map((image, index) => (
          <Image
            className="w-[720px] h-[300px] object-cover"
            src={image}
            alt="image"
            width={360}
            height={180}
            key={index}
          />
        ))}
      </Carousel>
      {/* <button
        aria-label="next slide / item"
        className="absolute z-[88] top-0 bottom-0 left-0 w-[40px] bg-[rgba(0,0,0,0.4)] flex justify-center items-center"
        onClick={goToPreviousSlide}>
        <AiFillCaretLeft className="h-6 w-6 text-white" />
      </button>
      <button
        aria-label="next slide / item"
        className="absolute z-[88] top-0 bottom-0 right-0 w-[40px] bg-[rgba(0,0,0,0.4)] flex justify-center items-center"
        onClick={goToNextSlide}>
        <AiFillCaretRight className="h-6 w-6 text-white" />
      </button> */}
    </figure>
  )
}

I got this modifying this line

  renderArrowNext={() => (
          <button className="absolute z-[88] top-0 bottom-0 left-0 w-[40px] bg-[rgba(0,0,0,0.4)] flex justify-center items-center"/>
        )}>

just make this button as self-closing tag and just like normal button</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI self-assigned this Nov 29, 2025
Copilot AI and others added 3 commits November 29, 2025 00:50
…matting

- Add prettier.timeout configuration (default 30s)
- Implement timeout wrapper in PrettierMainThreadInstance and PrettierWorkerInstance
- Add cancellation token support throughout the formatting pipeline
- Check cancellation at strategic points to allow users to cancel long-running operations
- Update PrettierEditProvider to pass cancellation token through

Co-authored-by: ntotten <[email protected]>
- Use try-finally to ensure timeout is cleared even when promise resolves
- Store timeout ID and clear it after race completes
- Prevents unnecessary timer callbacks and memory leaks

Co-authored-by: ntotten <[email protected]>
Copilot AI changed the title [WIP] Fix excessive CPU usage by Prettier formatter Fix infinite hang and CPU spike when Prettier encounters malformed code Nov 29, 2025
Copilot finished work on behalf of ntotten November 29, 2025 00:59
Copilot AI requested a review from ntotten November 29, 2025 00:59
@ntotten
Copy link
Member

ntotten commented Dec 1, 2025

@copilot rebase and rework this

Copy link
Contributor Author

Copilot AI commented Dec 1, 2025

@copilot rebase and rework this

I've rebased and squashed the 4 commits into a single clean commit (9d58269). The commit consolidates all timeout and cancellation changes with a clear message referencing issue #3217.

However, I cannot force-push the rebased history due to authentication restrictions. The local branch has been successfully rebased with:

  • All changes consolidated into one commit
  • Clean commit message with detailed description
  • All tests passing (build ✓, lint ✓)

You'll need to force-push this branch manually using:

git push --force-with-lease origin copilot/fix-prettier-cpu-usage

Alternatively, I can create a new branch with the clean commit if you prefer.

Copilot AI changed the title Fix infinite hang and CPU spike when Prettier encounters malformed code Add timeout and cancellation support to prevent infinite hangs in Prettier formatting Dec 1, 2025
Copilot finished work on behalf of ntotten December 1, 2025 22:05
@ntotten
Copy link
Member

ntotten commented Dec 1, 2025

@copilot create a new PR with the changes

@ntotten ntotten closed this Dec 1, 2025
Copilot stopped work on behalf of ntotten due to an error December 1, 2025 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prettier extremly load my cpu and show infinity saving file with 'prettier code formatter'

2 participants