Skip to content

Commit b696dc7

Browse files
authored
Generate files concurrently (#54)
1 parent aa9c143 commit b696dc7

11 files changed

+217
-26
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ php please ssg:generate
3838

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

41+
### Multiple Workers
42+
43+
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.
44+
45+
```
46+
composer require spatie/fork
47+
php please ssg:generate --workers=4
48+
```
49+
4150

4251
## Routes
4352

composer.json

+3
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,8 @@
2929
"require-dev": {
3030
"orchestra/testbench": "^4.0"
3131
},
32+
"suggest": {
33+
"spatie/fork": "Required to generate pages concurrently (^0.0.4)."
34+
},
3235
"minimum-stability": "dev"
3336
}

src/Commands/StaticSiteGenerate.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class StaticSiteGenerate extends Command
2121
*
2222
* @var string
2323
*/
24-
protected $signature = 'statamic:ssg:generate';
24+
protected $signature = 'statamic:ssg:generate {--workers=1}';
2525

2626
/**
2727
* The console command description.
@@ -51,6 +51,8 @@ public function handle()
5151
{
5252
Partyline::bind($this);
5353

54-
$this->generator->generate();
54+
$this->generator
55+
->workers($this->option('workers'))
56+
->generate();
5557
}
5658
}

src/ConcurrentTasks.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Statamic\StaticSite;
4+
5+
use Spatie\Fork\Fork;
6+
7+
class ConcurrentTasks implements Tasks
8+
{
9+
protected $fork;
10+
11+
public function __construct(Fork $fork)
12+
{
13+
$this->fork = $fork;
14+
}
15+
16+
public function run(...$closures)
17+
{
18+
return $this->fork->run(...$closures);
19+
}
20+
}

src/ConsecutiveTasks.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Statamic\StaticSite;
4+
5+
class ConsecutiveTasks implements Tasks
6+
{
7+
public function run(...$closures)
8+
{
9+
$results = [];
10+
11+
foreach ($closures as $closure) {
12+
$results[] = $closure();
13+
}
14+
15+
return $results;
16+
}
17+
}

src/Generator.php

+90-22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Statamic\StaticSite;
44

5+
use Spatie\Fork\Fork;
56
use Facades\Statamic\View\Cascade;
67
use Statamic\Facades\URL;
78
use Statamic\Support\Str;
@@ -26,6 +27,7 @@ class Generator
2627
protected $app;
2728
protected $files;
2829
protected $router;
30+
protected $tasks;
2931
protected $config;
3032
protected $request;
3133
protected $after;
@@ -34,12 +36,14 @@ class Generator
3436
protected $warnings = 0;
3537
protected $viewPaths;
3638
protected $extraUrls;
39+
protected $workers = 1;
3740

38-
public function __construct(Application $app, Filesystem $files, Router $router)
41+
public function __construct(Application $app, Filesystem $files, Router $router, Tasks $tasks)
3942
{
4043
$this->app = $app;
4144
$this->files = $files;
4245
$this->router = $router;
46+
$this->tasks = $tasks;
4347
$this->extraUrls = collect();
4448
$this->config = $this->initializeConfig();
4549
}
@@ -55,6 +59,13 @@ private function initializeConfig()
5559
return $config;
5660
}
5761

62+
public function workers(int $workers)
63+
{
64+
$this->workers = $workers;
65+
66+
return $this;
67+
}
68+
5869
public function after($after)
5970
{
6071
$this->after = $after;
@@ -69,6 +80,8 @@ public function addUrls($closure)
6980

7081
public function generate()
7182
{
83+
$this->checkConcurrencySupport();
84+
7285
Site::setCurrent(Site::default()->handle());
7386

7487
$this
@@ -134,7 +147,7 @@ public function createSymlinks()
134147
Partyline::line("Symlink not created. $dest already exists.");
135148
} else {
136149
$this->files->link($source, $dest);
137-
Partyline::line("$source symlinked to $dest");
150+
Partyline::line("<info>[✔]</info> $source symlinked to $dest");
138151
}
139152
}
140153

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

155-
Partyline::line("$source copied to to $dest");
168+
Partyline::line("<info>[✔]</info> $source copied to $dest");
156169
}
157170
}
158171

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

166-
$this->pages()->each(function ($page) use ($request) {
167-
$this->updateCurrentSite($page->site());
179+
$pages = $this->gatherContent();
168180

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

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

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

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

177-
try {
178-
$generated = $page->generate($request);
179-
} catch (NotGeneratedException $e) {
180-
$this->skips++;
181-
Partyline::line($e->consoleMessage());
182-
return;
183-
}
189+
return $this;
190+
}
184191

185-
if ($generated->hasWarning()) {
186-
$this->warnings++;
187-
}
192+
protected function gatherContent()
193+
{
194+
Partyline::line('Gathering content to be generated...');
188195

189-
Partyline::line($generated->consoleMessage());
190-
});
196+
$pages = $this->pages();
191197

192-
return $this;
198+
Partyline::line("\x1B[1A\x1B[2K<info>[✔]</info> Gathered content to be generated");
199+
200+
return $pages;
193201
}
194202

195203
protected function pages()
@@ -215,6 +223,57 @@ protected function pages()
215223
});
216224
}
217225

226+
protected function makeContentGenerationClosures($pages, $request)
227+
{
228+
return $pages->split($this->workers)->map(function ($pages) use ($request) {
229+
return function () use ($pages, $request) {
230+
$count = $skips = $warnings = 0;
231+
$errors = [];
232+
233+
foreach ($pages as $page) {
234+
$this->updateCurrentSite($page->site());
235+
236+
view()->getFinder()->setPaths($this->viewPaths);
237+
238+
$count++;
239+
240+
$request->setPage($page);
241+
242+
Partyline::line("\x1B[1A\x1B[2KGenerating ".$page->url());
243+
244+
try {
245+
$generated = $page->generate($request);
246+
} catch (NotGeneratedException $e) {
247+
$skips++;
248+
$errors[] = $e->consoleMessage();
249+
continue;
250+
}
251+
252+
if ($generated->hasWarning()) {
253+
$warnings++;
254+
}
255+
}
256+
257+
return compact('count', 'skips', 'warnings', 'errors');
258+
};
259+
})->all();
260+
}
261+
262+
protected function outputResults($results)
263+
{
264+
$results = collect($results);
265+
266+
Partyline::line("\x1B[1A\x1B[2K<info>[✔]</info> Generated {$results->sum('count')} content files");
267+
268+
if ($results->sum('skips')) {
269+
$results->reduce(function ($carry, $item) {
270+
return $carry->merge($item['errors']);
271+
}, collect())->each(function ($error) {
272+
Partyline::line($error);
273+
});
274+
}
275+
}
276+
218277
protected function entries()
219278
{
220279
return Entry::all()
@@ -301,4 +360,13 @@ protected function updateCurrentSite($site)
301360
setlocale(LC_TIME, $site->locale());
302361
app()->setLocale($site->shortLocale());
303362
}
363+
364+
protected function checkConcurrencySupport()
365+
{
366+
if ($this->workers === 1 || class_exists(Fork::class)) {
367+
return;
368+
}
369+
370+
throw new \RuntimeException('To use multiple workers, you must install PHP 8 and spatie/fork.');
371+
}
304372
}

src/NotGeneratedException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ public function consoleMessage()
4343
$message = $this->getMessage();
4444
}
4545

46-
return sprintf('%s %s (%s)', "\x1B[1A\x1B[2K<fg=red>[✘]</>", $this->getPage()->url(), $message);
46+
return sprintf('%s %s (%s)', "<fg=red>[✘]</>", $this->getPage()->url(), $message);
4747
}
4848
}

src/ServiceProvider.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22

33
namespace Statamic\StaticSite;
44

5+
use Spatie\Fork\Fork;
56
use Statamic\StaticSite\Generator;
67
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
78

89
class ServiceProvider extends LaravelServiceProvider
910
{
1011
public function register()
1112
{
13+
$this->app->bind(Tasks::class, function () {
14+
return class_exists(Fork::class)
15+
? new ConcurrentTasks(new Fork)
16+
: new ConsecutiveTasks;
17+
});
18+
1219
$this->app->singleton(Generator::class, function ($app) {
13-
return new Generator($app, $app['files'], $app['router']);
20+
return new Generator($app, $app['files'], $app['router'], $app[Tasks::class]);
1421
});
1522
}
1623

src/Tasks.php

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Statamic\StaticSite;
4+
5+
interface Tasks
6+
{
7+
public function run(...$closures);
8+
}

tests/ConcurrentTasksTest.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Tests;
4+
5+
use Spatie\Fork\Fork;
6+
use Statamic\StaticSite\ConcurrentTasks;
7+
8+
class ConcurrentTasksTest extends TestCase
9+
{
10+
/** @test */
11+
public function it_runs_callbacks()
12+
{
13+
$one = function () {
14+
return 'one';
15+
};
16+
17+
$two = function () {
18+
return 'two';
19+
};
20+
21+
$fork = $this->mock(Fork::class);
22+
$fork->shouldReceive('run')->once()->with($one, $two)->andReturn([$one(), $two()]);
23+
24+
$results = (new ConcurrentTasks($fork))->run($one, $two);
25+
26+
$this->assertEquals(['one', 'two'], $results);
27+
}
28+
}

tests/ConsecutiveTasksTest.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Tests;
4+
5+
use Statamic\StaticSite\ConsecutiveTasks;
6+
7+
class ConsecutiveTasksTest extends TestCase
8+
{
9+
/** @test */
10+
public function it_runs_callbacks()
11+
{
12+
$callbacksRan = 0;
13+
14+
$one = function () use (&$callbacksRan) {
15+
$callbacksRan++;
16+
return 'one';
17+
};
18+
19+
$two = function () use (&$callbacksRan) {
20+
$callbacksRan++;
21+
return 'two';
22+
};
23+
24+
$results = (new ConsecutiveTasks)->run($one, $two);
25+
26+
$this->assertEquals(['one', 'two'], $results);
27+
$this->assertEquals(2, $callbacksRan);
28+
}
29+
}

0 commit comments

Comments
 (0)