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

Generate files concurrently #54

Merged
merged 5 commits into from
Apr 28, 2021
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ php please ssg:generate

Your site will be generated into a directory which you can deploy however you like. See [Deployment Examples](#deployment-examples) below for inspiration.

### Multiple Workers

For improved performance, you may spread the page generation across multiple workers. This requires Spatie's [Fork](https://github.com/spatie/fork) package. Then you may specify how many workers are to be used. You can use as many workers as you have CPU cores.

```
composer require spatie/fork
php please ssg:generate --workers=4
```


## Routes

Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@
"require-dev": {
"orchestra/testbench": "^4.0"
},
"suggest": {
"spatie/fork": "Required to generate pages concurrently (^0.0.4)."
},
"minimum-stability": "dev"
}
6 changes: 4 additions & 2 deletions src/Commands/StaticSiteGenerate.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class StaticSiteGenerate extends Command
*
* @var string
*/
protected $signature = 'statamic:ssg:generate';
protected $signature = 'statamic:ssg:generate {--workers=1}';

/**
* The console command description.
Expand Down Expand Up @@ -51,6 +51,8 @@ public function handle()
{
Partyline::bind($this);

$this->generator->generate();
$this->generator
->workers($this->option('workers'))
->generate();
}
}
20 changes: 20 additions & 0 deletions src/ConcurrentTasks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Statamic\StaticSite;

use Spatie\Fork\Fork;

class ConcurrentTasks implements Tasks
{
protected $fork;

public function __construct(Fork $fork)
{
$this->fork = $fork;
}

public function run(...$closures)
{
return $this->fork->run(...$closures);
}
}
17 changes: 17 additions & 0 deletions src/ConsecutiveTasks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Statamic\StaticSite;

class ConsecutiveTasks implements Tasks
{
public function run(...$closures)
{
$results = [];

foreach ($closures as $closure) {
$results[] = $closure();
}

return $results;
}
}
112 changes: 90 additions & 22 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\StaticSite;

use Spatie\Fork\Fork;
use Facades\Statamic\View\Cascade;
use Statamic\Facades\URL;
use Statamic\Support\Str;
Expand All @@ -26,6 +27,7 @@ class Generator
protected $app;
protected $files;
protected $router;
protected $tasks;
protected $config;
protected $request;
protected $after;
Expand All @@ -34,12 +36,14 @@ class Generator
protected $warnings = 0;
protected $viewPaths;
protected $extraUrls;
protected $workers = 1;

public function __construct(Application $app, Filesystem $files, Router $router)
public function __construct(Application $app, Filesystem $files, Router $router, Tasks $tasks)
{
$this->app = $app;
$this->files = $files;
$this->router = $router;
$this->tasks = $tasks;
$this->extraUrls = collect();
$this->config = $this->initializeConfig();
}
Expand All @@ -55,6 +59,13 @@ private function initializeConfig()
return $config;
}

public function workers(int $workers)
{
$this->workers = $workers;

return $this;
}

public function after($after)
{
$this->after = $after;
Expand All @@ -69,6 +80,8 @@ public function addUrls($closure)

public function generate()
{
$this->checkConcurrencySupport();

Site::setCurrent(Site::default()->handle());

$this
Expand Down Expand Up @@ -134,7 +147,7 @@ public function createSymlinks()
Partyline::line("Symlink not created. $dest already exists.");
} else {
$this->files->link($source, $dest);
Partyline::line("$source symlinked to $dest");
Partyline::line("<info>[✔]</info> $source symlinked to $dest");
}
}

Expand All @@ -152,7 +165,7 @@ public function copyFiles()
$this->files->copyDirectory($source, $dest);
}

Partyline::line("$source copied to to $dest");
Partyline::line("<info>[✔]</info> $source copied to $dest");
}
}

Expand All @@ -163,33 +176,28 @@ protected function createContentFiles()
$this->app->instance('request', $request);
});

$this->pages()->each(function ($page) use ($request) {
$this->updateCurrentSite($page->site());
$pages = $this->gatherContent();

view()->getFinder()->setPaths($this->viewPaths);
Partyline::line("Generating {$pages->count()} content files...");

$this->count++;
$closures = $this->makeContentGenerationClosures($pages, $request);

$request->setPage($page);
$results = $this->tasks->run(...$closures);

Partyline::comment("Generating {$page->url()}...");
$this->outputResults($results);

try {
$generated = $page->generate($request);
} catch (NotGeneratedException $e) {
$this->skips++;
Partyline::line($e->consoleMessage());
return;
}
return $this;
}

if ($generated->hasWarning()) {
$this->warnings++;
}
protected function gatherContent()
{
Partyline::line('Gathering content to be generated...');

Partyline::line($generated->consoleMessage());
});
$pages = $this->pages();

return $this;
Partyline::line("\x1B[1A\x1B[2K<info>[✔]</info> Gathered content to be generated");

return $pages;
}

protected function pages()
Expand All @@ -215,6 +223,57 @@ protected function pages()
});
}

protected function makeContentGenerationClosures($pages, $request)
{
return $pages->split($this->workers)->map(function ($pages) use ($request) {
return function () use ($pages, $request) {
$count = $skips = $warnings = 0;
$errors = [];

foreach ($pages as $page) {
$this->updateCurrentSite($page->site());

view()->getFinder()->setPaths($this->viewPaths);

$count++;

$request->setPage($page);

Partyline::line("\x1B[1A\x1B[2KGenerating ".$page->url());

try {
$generated = $page->generate($request);
} catch (NotGeneratedException $e) {
$skips++;
$errors[] = $e->consoleMessage();
continue;
}

if ($generated->hasWarning()) {
$warnings++;
}
}

return compact('count', 'skips', 'warnings', 'errors');
};
})->all();
}

protected function outputResults($results)
{
$results = collect($results);

Partyline::line("\x1B[1A\x1B[2K<info>[✔]</info> Generated {$results->sum('count')} content files");

if ($results->sum('skips')) {
$results->reduce(function ($carry, $item) {
return $carry->merge($item['errors']);
}, collect())->each(function ($error) {
Partyline::line($error);
});
}
}

protected function entries()
{
return Entry::all()
Expand Down Expand Up @@ -301,4 +360,13 @@ protected function updateCurrentSite($site)
setlocale(LC_TIME, $site->locale());
app()->setLocale($site->shortLocale());
}

protected function checkConcurrencySupport()
{
if ($this->workers === 1 || class_exists(Fork::class)) {
return;
}

throw new \RuntimeException('To use multiple workers, you must install PHP 8 and spatie/fork.');
}
}
2 changes: 1 addition & 1 deletion src/NotGeneratedException.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ public function consoleMessage()
$message = $this->getMessage();
}

return sprintf('%s %s (%s)', "\x1B[1A\x1B[2K<fg=red>[✘]</>", $this->getPage()->url(), $message);
return sprintf('%s %s (%s)', "<fg=red>[✘]</>", $this->getPage()->url(), $message);
}
}
9 changes: 8 additions & 1 deletion src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

namespace Statamic\StaticSite;

use Spatie\Fork\Fork;
use Statamic\StaticSite\Generator;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;

class ServiceProvider extends LaravelServiceProvider
{
public function register()
{
$this->app->bind(Tasks::class, function () {
return class_exists(Fork::class)
? new ConcurrentTasks(new Fork)
: new ConsecutiveTasks;
});

$this->app->singleton(Generator::class, function ($app) {
return new Generator($app, $app['files'], $app['router']);
return new Generator($app, $app['files'], $app['router'], $app[Tasks::class]);
});
}

Expand Down
8 changes: 8 additions & 0 deletions src/Tasks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Statamic\StaticSite;

interface Tasks
{
public function run(...$closures);
}
28 changes: 28 additions & 0 deletions tests/ConcurrentTasksTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Tests;

use Spatie\Fork\Fork;
use Statamic\StaticSite\ConcurrentTasks;

class ConcurrentTasksTest extends TestCase
{
/** @test */
public function it_runs_callbacks()
{
$one = function () {
return 'one';
};

$two = function () {
return 'two';
};

$fork = $this->mock(Fork::class);
$fork->shouldReceive('run')->once()->with($one, $two)->andReturn([$one(), $two()]);

$results = (new ConcurrentTasks($fork))->run($one, $two);

$this->assertEquals(['one', 'two'], $results);
}
}
29 changes: 29 additions & 0 deletions tests/ConsecutiveTasksTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Tests;

use Statamic\StaticSite\ConsecutiveTasks;

class ConsecutiveTasksTest extends TestCase
{
/** @test */
public function it_runs_callbacks()
{
$callbacksRan = 0;

$one = function () use (&$callbacksRan) {
$callbacksRan++;
return 'one';
};

$two = function () use (&$callbacksRan) {
$callbacksRan++;
return 'two';
};

$results = (new ConsecutiveTasks)->run($one, $two);

$this->assertEquals(['one', 'two'], $results);
$this->assertEquals(2, $callbacksRan);
}
}