Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Bug Fix",
"description": "Fixed a crash when trying to use Q /dev on large projects or projects containing files with unsupported encoding."
}
3 changes: 3 additions & 0 deletions packages/core/src/amazonqFeatureDev/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const featureDevChat = 'featureDevChat'

export const featureName = 'Amazon Q feature development'

// Max allowed size for file collection
export const maxRepoSizeBytes = 200 * 1024 * 1024

// License text that's used in codewhisperer reference log
export const referenceLogText = (reference: CodeReference) =>
`[${new Date().toLocaleString()}] Accepted recommendation from Amazon Q. Code provided with reference under <a href="${LicenseUtil.getLicenseHtml(
Expand Down
60 changes: 42 additions & 18 deletions packages/core/src/amazonqFeatureDev/util/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Uri } from 'vscode'
import { GitIgnoreFilter } from './gitignore'

import AdmZip from 'adm-zip'
import { PrepareRepoFailedError } from '../errors'
import { ContentLengthError, PrepareRepoFailedError } from '../errors'
import { getLogger } from '../../shared/logger/logger'
import { maxFileSizeBytes } from '../limits'
import { createHash } from 'crypto'
Expand All @@ -21,6 +21,7 @@ import { ToolkitError } from '../../shared/errors'
import { AmazonqCreateUpload, Metric } from '../../shared/telemetry/telemetry'
import { TelemetryHelper } from './telemetryHelper'
import { sanitizeFilename } from '../../shared/utilities/textUtilities'
import { maxRepoSizeBytes } from '../constants'

export function getExcludePattern(additionalPatterns: string[] = []) {
const globAlwaysExcludedDirs = getGlobDirExcludedPatterns().map(pattern => `**/${pattern}/*`)
Expand Down Expand Up @@ -94,6 +95,7 @@ export async function collectFiles(
return prefix === '' ? path : `${prefix}/${path}`
}

let totalSizeBytes = 0
for (const rootPath of sourcePaths) {
const allFiles = await vscode.workspace.findFiles(
new vscode.RelativePattern(rootPath, '**'),
Expand All @@ -102,29 +104,48 @@ export async function collectFiles(
const files = respectGitIgnore ? await filterOutGitignoredFiles(rootPath, allFiles) : allFiles

for (const file of files) {
try {
const fileContent = await SystemUtilities.readFile(file, new TextDecoder('utf8', { fatal: true }))
const relativePath = getWorkspaceRelativePath(file.fsPath, { workspaceFolders })
const relativePath = getWorkspaceRelativePath(file.fsPath, { workspaceFolders })
if (!relativePath) {
continue
}

if (relativePath) {
storage.push({
workspaceFolder: relativePath.workspaceFolder,
relativeFilePath: relativePath.relativePath,
fileUri: file,
fileContent: fileContent,
zipFilePath: prefixWithFolderPrefix(relativePath.workspaceFolder, relativePath.relativePath),
})
}
} catch (error) {
getLogger().debug(
`featureDev: Failed to read file ${file.fsPath} when collecting repository: ${error}. Skipping the file`
)
const fileStat = await vscode.workspace.fs.stat(file)
if (totalSizeBytes + fileStat.size > maxRepoSizeBytes) {
throw new ContentLengthError()
}

const fileContent = await readFile(file)
if (fileContent === undefined) {
continue
}

// Now that we've read the file, increase our usage
totalSizeBytes += fileStat.size
storage.push({
workspaceFolder: relativePath.workspaceFolder,
relativeFilePath: relativePath.relativePath,
fileUri: file,
fileContent: fileContent,
zipFilePath: prefixWithFolderPrefix(relativePath.workspaceFolder, relativePath.relativePath),
})
}
}
return storage
}

const readFile = async (file: vscode.Uri) => {
try {
const fileContent = await SystemUtilities.readFile(file, new TextDecoder('utf8', { fatal: false }))
return fileContent
} catch (error) {
getLogger().debug(
`featureDev: Failed to read file ${file.fsPath} when collecting repository. Skipping the file`
)
}

return undefined
}

const getSha256 = (file: Buffer) => createHash('sha256').update(file).digest('base64')

/**
Expand All @@ -137,9 +158,9 @@ export async function prepareRepoData(
span: Metric<AmazonqCreateUpload>
) {
try {
const files = await collectFiles(repoRootPaths, workspaceFolders, true)
const zip = new AdmZip()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the current approach used to zip files does not "stream" the zip creation so will also run into memory constraints.

please look at #4769 , can you rewrite this feature on top of that (after it lands)?

Copy link
Contributor

@grimalk grimalk Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the ETA of this being ready? Presume we want both these in the same wave of the toolkit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm happy to change our implementation to use the stream after it's merged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the ETA of this being ready?

The zip streaming won't be ready today, but hopefully next week. @ctlai95


const files = await collectFiles(repoRootPaths, workspaceFolders, true)
let totalBytes = 0
for (const file of files) {
const fileSize = (await vscode.workspace.fs.stat(file.fileUri)).size
Expand All @@ -163,6 +184,9 @@ export async function prepareRepoData(
}
} catch (error) {
getLogger().debug(`featureDev: Failed to prepare repo: ${error}`)
if (error instanceof ContentLengthError) {
throw error
}
throw new PrepareRepoFailedError()
}
}
Expand Down