async function psiCollectForProject(storageMethod, psi, site) {
  const {
    urls,
    projectSlug,
    numberOfRuns = 5,
    maxNumberOfParallelUrls = Infinity,
    strategy = 'mobile',
  } = site;
  const project = await storageMethod.findProjectBySlug(projectSlug);
  if (!project) throw new Error(`Invalid project slug "${projectSlug}"`);
  if (!urls || !urls.length) throw new Error('No URLs set');
  if (maxNumberOfParallelUrls < 1) {
    throw new Error('maxNumberOfParallelUrls must be a positive integer > 0');
  }

  const build = await storageMethod.createBuild({
    projectId: project.id,
    lifecycle: 'unsealed',
    branch: site.branch || project.baseBranch,
    externalBuildUrl: urls[0],

    commitMessage: `Autocollected at ${new Date().toLocaleString()}`,
    author: `Lighthouse CI Server <no-reply@example.com>`,
    avatarUrl: getGravatarUrlFromEmail('no-reply@example.com'),
    hash: Date.now().toString(16).split('').reverse().join(''),

    runAt: new Date().toISOString(),
    committedAt: new Date().toISOString(),
  });

  // Run 'maxNumberOfParallelUrls' URLs in parallel
  await Bluebird.map(
    urls,
    async url => {
      for (let i = 0; i < numberOfRuns; i++) {
        const lhr = await psi.runUntilSuccess(url, {
          psiStrategy: strategy,
          psiCategories: site.categories,
        });
        await storageMethod.createRun({
          projectId: project.id,
          buildId: build.id,
          representative: false,
          url,
          lhr,
        });

        // If we're not on the last one, wait at least 60s before trying again to cachebust PSI
        if (i !== numberOfRuns - 1) {
          await new Promise(r => setTimeout(r, psi.CACHEBUST_TIMEOUT));
        }
      }
    },
    {concurrency: maxNumberOfParallelUrls}
  );

  await storageMethod.sealBuild(build.projectId, build.id);
}
@param {LHCI.ServerCommand.StorageMethod} storageMethod
@param {PsiRunner} psi
@param {LHCI.ServerCommand.PsiCollectEntry} site
@return {Promise<void>}