photish 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 837da75e8c5426ba38888369dc28eaa433e5162c
4
- data.tar.gz: 8678a47a42f4b7e49ec3faae98130ada68e742e6
3
+ metadata.gz: 5ff58bf1d669264455034b1ea27a798ea979d2f0
4
+ data.tar.gz: 6f6bb07a9b0ecd54643b9090c27336b13c0ecb09
5
5
  SHA512:
6
- metadata.gz: f5fb8362d0d569a7f2f7c13edcd86259112cc062714f64a30602d3d00aaf7e6efde8bf0f5753a72a7f92447702b67d823085c363e445defddee223d98341374d
7
- data.tar.gz: e5309014399f150ee267288433e363206b6b9356c4f6c7fbe28d9a61f9cbdd00112d0cb01017a890b6e7c946bd8488d12f2f2e37a04f1ca5987cdd1d539f5bad
6
+ metadata.gz: ea5d2478db86db55aca53b8fec1abe53c65d6714e6fa12bd6d3c8cd85b5f4d145183e184b4e9a10bf1a8b9cd0fd4650426ae4416f2862b7c54bae49953881663
7
+ data.tar.gz: b86aa902da22f12807bdef9d26b46f7df180472268e56affc1ea9b63a7ee0c2f45fd15056a64c3a049bcb9a8acf05c6c99f54e7a3c185ce801f725f7bd72dfb6
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  Gemfile.lock
4
4
  _yardoc
5
5
  coverage
6
+ profile
6
7
  doc
7
8
  pkg
8
9
  spec/reports
data/README.md CHANGED
@@ -34,9 +34,8 @@ It is strongly recommended to read through the [Installation](#installation)
34
34
  and [Usage](#usage) sections before seriously using Photish, however to get up
35
35
  and running:
36
36
 
37
- 1. Ensure [ImageMagick](http://www.imagemagick.org/script/index.php),
38
- [Exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/) and
39
- [Nokogiri](http://www.nokogiri.org/tutorials/installing_nokogiri.html) are
37
+ 1. Ensure [ImageMagick](http://www.imagemagick.org/script/index.php) and
38
+ [Exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/) are
40
39
  installed (see [Dependencies](#dependencies))
41
40
  1. Install Photish `gem install photish`
42
41
  1. Create a base project with `photish init --example`
@@ -63,6 +62,8 @@ and running:
63
62
  - [Template Helpers](#template-helpers)
64
63
  - [Generate](#generate)
65
64
  - [Execution Order](#execution-order)
65
+ - [Workers and Threads](#workers-and-threads)
66
+ - [Caching](#caching)
66
67
  - [Host](#host)
67
68
  - [Rake Task](#rake-task)
68
69
  - [Plugins](#plugins)
@@ -191,8 +192,6 @@ Photish has dependencies on certain utilities:
191
192
  conversion
192
193
  - [Exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/) for image metadata
193
194
  retrieval
194
- - [Nokogiri](http://www.nokogiri.org/tutorials/installing_nokogiri.html) for
195
- XML writing and parsing
196
195
 
197
196
  **On MacOSX, using [Brew](http://brew.sh/)**
198
197
 
@@ -293,6 +292,8 @@ logging:
293
292
  url:
294
293
  host: http://mydomain.com
295
294
  base: 'subdirectory'
295
+ workers: 4
296
+ threads: 2
296
297
  ```
297
298
 
298
299
  The meanings and purpose of each field is defined below:
@@ -315,6 +316,8 @@ Field | Purpose
315
316
  `url` | a listing of the various url options
316
317
  `url/host` | if you would like URLs generated with a specific host prefix, you can define it here, otherwise leave it as '/' or do not set this configuration at all
317
318
  `url/base` | if your website will be hosted in a sub folder and will not be accessible at the root of the host, you can specify the sub folder(s) here, this will also mean your website will be hosted in a sub folder when ran using `photish host`
319
+ `workers` | the number of workers to create, for computers with multiple processors, photish is configured by default to spawn a worker for each process, a worker is responsible for image generation and html generation, load balancing is done randomly via a simple round robin allocation
320
+ `threads` | the number of threads each worker should create to handle image magick transcoding
318
321
 
319
322
  #### Customizing Templates
320
323
 
@@ -468,6 +471,55 @@ The Generate command does the following:
468
471
  1. Converts all Photo(s) to the configured quality versions, writing various
469
472
  images to the `output` folder
470
473
 
474
+ #### Workers and Threads
475
+
476
+ In order to achieve maximum utilization of all processors on a CPU during
477
+ generation, Photish has the ability to create multiple workers and threads.
478
+
479
+ A worker is a spawned sub process created by the Generate command. The worker
480
+ sub process is responsible for generating the HTML and Images for a sub set of
481
+ the collection.
482
+
483
+ Within each worker, threads are created when calling out to the Image Magick
484
+ binary. During conversion, Image Magick often does not reach full processor
485
+ utilization so rather then block the whole worker, it can be more performant to
486
+ spawn multiple Image Magick processes at once.
487
+
488
+ For collections with a large number of images and HTML pages, multiple workers
489
+ and threads can be used to rapidly speed up generation. However if the
490
+ collection is quite small and the images are of a small size, workers and
491
+ threads will increase the generation time as loading a new ruby runtime and
492
+ creating multiple threads may have a higher setup time then just generating in
493
+ a single ruby process.
494
+
495
+ The number of workers and threads is configurable in the [config
496
+ file](#config-file-options) with the `workers` and `threads` options. By
497
+ default, Photish will spawn a worker for each processor detected on the
498
+ computer. It will then create 2 threads per worker. As each worker spawns it's
499
+ own thread, for a computer with 4 processors, 4 workers will be created, each
500
+ with 2 threads, which means in total Photish will manage 8 threads and
501
+ potentially run 8 Image Magick processes concurrently. When tweaking the number
502
+ of workers and threads it is important to consider IO bottlenecks as this will
503
+ most likely be the limiting factor in performance.
504
+
505
+ #### Caching
506
+
507
+ Photish caches the generation of images to avoid regeneration when the
508
+ Generate command is run or the generate event is triggered while hosting
509
+ a local version of Photish with the Host command.
510
+
511
+ The cache file is stored in the `output_dir` and is named `.changes.yml`.
512
+
513
+ To do a full regeneration, simple run the Generate command with the `force`
514
+ flag:
515
+
516
+ $ photish generate --force
517
+
518
+ Images are regenerated when they are modified, renamed or moved.
519
+
520
+ Changing the `qualities` option in the config file will also trigger a full
521
+ regeneration of all images.
522
+
471
523
  ### Host
472
524
 
473
525
  To test and view your changes locally, the host command can be used to run a
@@ -521,8 +573,6 @@ called inside the template, it will render the message in bold wrapped in the
521
573
 
522
574
  **site/_plugins/shout.rb**
523
575
  ```ruby
524
- require 'nokogiri'
525
-
526
576
  module Photish::Plugin::Shout
527
577
 
528
578
  def self.is_for?(type)
data/TODO.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## In Progress
4
4
 
5
+ 1. Look at error handling, exiting and cleanup (files, processes and threads)
6
+
5
7
  ## Backlog
6
8
 
7
9
  1. Plugin as a gem to deploy to github pages, netlify, amazon s3
data/exe/photish CHANGED
@@ -1,10 +1,24 @@
1
1
  #!/usr/bin/env ruby
2
+
2
3
  if ENV['COVERAGE']
3
4
  require 'simplecov'
4
-
5
5
  SimpleCov.command_name "photish-binary-#{Process.pid}"
6
6
  SimpleCov.root(File.join(File.expand_path(File.dirname(__FILE__)), '..'))
7
7
  end
8
8
 
9
+ if ENV['PROFILE']
10
+ require 'ruby-prof'
11
+ FileUtils.rm_rf('profile')
12
+ RubyProf.start
13
+ end
14
+
9
15
  require 'photish'
10
16
  Photish::CLI::Interface.start
17
+
18
+ if ENV['PROFILE']
19
+ profile_filename = "cmd-#{ARGV[0] || 'unknown'}-#{Process.pid}"
20
+ FileUtils.mkdir_p('profile')
21
+ RubyProf::MultiPrinter.new(RubyProf.stop)
22
+ .print(profile: profile_filename,
23
+ path: 'profile')
24
+ end
@@ -1,5 +1,3 @@
1
- require 'nokogiri'
2
-
3
1
  module Photish::Plugin::FooterLinks
4
2
 
5
3
  def self.is_for?(type)
@@ -12,19 +10,16 @@ module Photish::Plugin::FooterLinks
12
10
  end
13
11
 
14
12
  def links_with_seperator(links, seperator)
15
- doc = Nokogiri::HTML::DocumentFragment.parse("")
16
- Nokogiri::HTML::Builder.with(doc) do |doc|
17
- links.each_with_index do |link, index|
18
- if index == (links.count - 1)
19
- text = link[:text]
20
- else
21
- text = "#{link[:text]} #{seperator} "
22
- end
23
-
24
- doc.a(text, href: link[:url])
13
+ html = ''
14
+ links.each_with_index do |link, index|
15
+ if index == (links.count - 1)
16
+ text = link[:text]
17
+ else
18
+ text = "#{link[:text]} #{seperator} "
25
19
  end
20
+ html << "<a href=\"" << link[:url] << "\">" << text << "</a>"
26
21
  end
27
- doc.to_html
22
+ html
28
23
  end
29
24
  end
30
25
 
@@ -1,5 +1,3 @@
1
- require 'nokogiri'
2
-
3
1
  module Photish::Plugin::YellLoud
4
2
 
5
3
  def self.is_for?(type)
@@ -12,11 +10,7 @@ module Photish::Plugin::YellLoud
12
10
  end
13
11
 
14
12
  def yell_very_loud
15
- doc = Nokogiri::HTML::DocumentFragment.parse("")
16
- Nokogiri::HTML::Builder.with(doc) do |doc|
17
- doc.span("Yelling \"#{name}\" from a plugin!",
18
- style: 'font-weight:bold;color:red;font-size:200%;')
19
- end
20
- doc.to_html
13
+ text = "Yelling \"#{name}\" from a plugin!"
14
+ "<span style=\"font-weight:bold;color:red;font-size:200%;\">#{text}</span>"
21
15
  end
22
16
  end
@@ -49,7 +49,6 @@ ul.breadcrumbs, ul.breadcrumbs li {
49
49
  padding:0;
50
50
  }
51
51
  ul.breadcrumbs {
52
- border:1px solid #dedede;
53
52
  height:2.3em;
54
53
  }
55
54
  ul.breadcrumbs li {
@@ -0,0 +1,61 @@
1
+ module Photish
2
+ module Cache
3
+ class Manifest
4
+ def initialize(output_dir, worker_index, version_hash)
5
+ @output_dir = output_dir
6
+ @worker_index = worker_index
7
+ @version_hash = version_hash
8
+ @cache = {}
9
+ @worker_db = {}
10
+ end
11
+
12
+ def record(key, file_path = nil)
13
+ checksum = checksum_of_file(file_path || key)
14
+ worker_db[key] = checksum
15
+ db[key] = checksum
16
+ end
17
+
18
+ def changed?(key, file_path = nil)
19
+ checksum = checksum_of_file(file_path || key)
20
+ worker_db[key] = checksum
21
+ checksum != db[key]
22
+ end
23
+
24
+ def flush_to_disk
25
+ File.open(worker_db_file, 'w') { |f| f.write(worker_db.to_yaml) }
26
+ end
27
+
28
+ def preload
29
+ db
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :output_dir,
35
+ :cache,
36
+ :version_hash,
37
+ :worker_index,
38
+ :worker_db
39
+
40
+ def checksum_of_file(file_path)
41
+ cache.fetch(file_path.hash) do |key|
42
+ cache[key] = version_hash.to_s +
43
+ Digest::MD5.file(file_path).hexdigest
44
+ end
45
+ end
46
+
47
+ def db
48
+ return @db if @db
49
+ @db = File.exist?(db_file) ? YAML.load_file(db_file) : {}
50
+ end
51
+
52
+ def db_file
53
+ ManifestDbFile.db_file(output_dir)
54
+ end
55
+
56
+ def worker_db_file
57
+ ManifestDbFile.worker_db_file(output_dir, worker_index)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,28 @@
1
+ module Photish
2
+ module Cache
3
+ module ManifestDbFile
4
+ def concat_db_files(output_dir, workers)
5
+ changes = (1..workers).inject({}) do |accumulator, worker_index|
6
+ file = worker_db_file(output_dir, worker_index)
7
+ accumulator.merge!(YAML.load_file(file)) if File.exist?(file)
8
+ accumulator
9
+ end
10
+ File.open(db_file(output_dir), 'w') { |f| f.write(changes.to_yaml) }
11
+ end
12
+
13
+ def db_file(output_dir)
14
+ FileUtils.mkdir_p(output_dir)
15
+ File.join(output_dir, '.changes.yml')
16
+ end
17
+
18
+ def worker_db_file(output_dir, index)
19
+ FileUtils.mkdir_p(output_dir)
20
+ File.join(output_dir, ".changes.#{index}.yml")
21
+ end
22
+
23
+ module_function :concat_db_files,
24
+ :db_file,
25
+ :worker_db_file
26
+ end
27
+ end
28
+ end
@@ -4,10 +4,17 @@ module Photish
4
4
  package_name "Photish"
5
5
 
6
6
  desc "generate", "Generates the gallery static site"
7
+ option :worker_index, type: :numeric
7
8
  def generate
8
9
  Photish::Command::Generate.new(options).execute
9
10
  end
10
11
 
12
+ desc "worker", "A worker process that helps the generate command"
13
+ option :worker_index, type: :numeric
14
+ def worker
15
+ Photish::Command::Worker.new(options).execute
16
+ end
17
+
11
18
  desc "host", "Serves the HTML on a HTTP server"
12
19
  def host
13
20
  Photish::Command::Host.new(options).execute
@@ -21,15 +21,18 @@ module Photish
21
21
  attr_reader :runtime_config,
22
22
  :log
23
23
 
24
- def config
25
- @config ||= Photish::Config::AppSettings.new(runtime_config)
26
- .config
27
- end
24
+ delegate :config,
25
+ :version_hash,
26
+ to: :app_settings
28
27
 
29
28
  private
30
29
 
30
+ def app_settings
31
+ @app_settings ||= Config::AppSettings.new(runtime_config)
32
+ end
33
+
31
34
  def setup_logging
32
- Photish::Log::Logger.instance.setup_logging(config)
35
+ Log::Logger.instance.setup_logging(config)
33
36
  end
34
37
  end
35
38
  end
@@ -23,7 +23,7 @@ module Photish
23
23
  end
24
24
 
25
25
  def load_all_plugins
26
- Photish::Plugin::Repository.reload(log, site_dir)
26
+ Plugin::Repository.reload(log, site_dir)
27
27
  end
28
28
 
29
29
  def engine_class
@@ -33,11 +33,11 @@ module Photish
33
33
  end
34
34
 
35
35
  def deploy_plugins
36
- Photish::Plugin::Repository.plugins_for(deploy_plugin_type)
36
+ Plugin::Repository.plugins_for(deploy_plugin_type)
37
37
  end
38
38
 
39
39
  def deploy_plugin_type
40
- Photish::Plugin::Type::Deploy
40
+ Plugin::Type::Deploy
41
41
  end
42
42
  end
43
43
  end
@@ -2,32 +2,57 @@ module Photish
2
2
  module Command
3
3
  class Generate < Base
4
4
  def run
5
+ log.info "Starting generation with #{workers} workers"
6
+
7
+ spawn_all_workers
5
8
  load_all_plugins
6
- render_whole_site
7
- log.info 'Site generation completed successfully'
9
+ wait_for_workers_to_complete
10
+ concat_db_files
11
+ perform_serial_generation
12
+
13
+ log.info "Generation completed successfully"
8
14
  end
9
15
 
10
16
  private
11
17
 
12
18
  delegate :output_dir,
13
- :site_dir,
14
19
  :photo_dir,
15
- :qualities,
16
- :templates,
17
20
  :url,
18
- :max_workers,
21
+ :site_dir,
22
+ :qualities,
23
+ :photish_executable,
24
+ :workers,
19
25
  to: :config
20
26
 
21
27
  def load_all_plugins
22
- Photish::Plugin::Repository.reload(log, site_dir)
28
+ Plugin::Repository.reload(log, site_dir)
29
+ end
30
+
31
+ def spawn_all_workers
32
+ return single_worker if one_worker?
33
+ @spawned_processes ||= (1..workers).map do |index|
34
+ Process.spawn(ENV, worker_command(index))
35
+ end
36
+ end
37
+
38
+ def wait_for_workers_to_complete
39
+ return if one_worker?
40
+ @spawned_processes.map do |pid|
41
+ Process.waitpid(pid)
42
+ end
43
+ end
44
+
45
+ def one_worker?
46
+ workers == 1
47
+ end
48
+
49
+ def single_worker
50
+ Worker.new(runtime_config.merge(worker_index: 1)).execute
23
51
  end
24
52
 
25
- def render_whole_site
26
- Photish::Render::Site.new(templates,
27
- site_dir,
28
- output_dir,
29
- max_workers)
30
- .all_for(collection)
53
+ def perform_serial_generation
54
+ Render::Site.new(config)
55
+ .all_for(collection)
31
56
  end
32
57
 
33
58
  def collection
@@ -39,6 +64,16 @@ module Photish
39
64
  def qualities_mapped
40
65
  qualities.map { |quality| OpenStruct.new(quality) }
41
66
  end
67
+
68
+ def worker_command(worker_index)
69
+ [photish_executable,
70
+ 'worker',
71
+ "--worker_index=#{worker_index}"].join(' ')
72
+ end
73
+
74
+ def concat_db_files
75
+ Cache::ManifestDbFile.concat_db_files(output_dir, workers)
76
+ end
42
77
  end
43
78
  end
44
79
  end
@@ -8,6 +8,7 @@ module Photish
8
8
  log.info "Monitoring paths #{paths_to_monitor}"
9
9
 
10
10
  regenerate_entire_site
11
+ regenerate_thread
11
12
  start_http_server_with_listener
12
13
  end
13
14
 
@@ -17,14 +18,27 @@ module Photish
17
18
  :output_dir,
18
19
  :site_dir,
19
20
  :photo_dir,
21
+ :config_file_location,
20
22
  to: :config
21
23
 
22
24
  def start_http_server_with_listener
23
25
  trap 'INT' do server.shutdown end
24
26
  listener.start
25
27
  server.start
26
- listener.stop
27
28
  log.info "Photish host has shutdown"
29
+ ensure
30
+ regenerate_thread.exit if @regenerate_thread
31
+ listener.stop if @listener
32
+ end
33
+
34
+ def regenerate_thread
35
+ @regenerate_thread ||= Thread.new do
36
+ loop do
37
+ queue.pop
38
+ queue.clear
39
+ regenerate_entire_site
40
+ end
41
+ end
28
42
  end
29
43
 
30
44
  def server
@@ -39,8 +53,7 @@ module Photish
39
53
  log.info "File was modified #{modified}" if modified.present?
40
54
  log.info "File was added #{added}" if added.present?
41
55
  log.info "File was removed #{removed}" if removed.present?
42
-
43
- regenerate_entire_site
56
+ queue.push(modified || added || removed)
44
57
  end
45
58
  end
46
59
 
@@ -61,6 +74,10 @@ module Photish
61
74
  Photish::Command::Generate.new(runtime_config)
62
75
  .execute
63
76
  end
77
+
78
+ def queue
79
+ @queue ||= Queue.new
80
+ end
64
81
  end
65
82
  end
66
83
  end
@@ -0,0 +1,44 @@
1
+ module Photish
2
+ module Command
3
+ class Worker < Base
4
+ def run
5
+ log.info "Worker ##{worker_index} starting"
6
+
7
+ load_all_plugins
8
+ render_whole_site
9
+
10
+ log.info "Site generation completed, by Worker ##{worker_index}"
11
+ end
12
+
13
+ private
14
+
15
+ delegate :site_dir,
16
+ :photo_dir,
17
+ :output_dir,
18
+ :qualities,
19
+ :url,
20
+ :worker_index,
21
+ to: :config
22
+
23
+ def load_all_plugins
24
+ return if Plugin::Repository.loaded?
25
+ Plugin::Repository.reload(log, site_dir)
26
+ end
27
+
28
+ def render_whole_site
29
+ Render::SiteWorker.new(config, version_hash)
30
+ .all_for(collection)
31
+ end
32
+
33
+ def collection
34
+ @collection ||= Gallery::Collection.new(photo_dir,
35
+ qualities_mapped,
36
+ url)
37
+ end
38
+
39
+ def qualities_mapped
40
+ qualities.map { |quality| OpenStruct.new(quality) }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -9,6 +9,10 @@ module Photish
9
9
  @config ||= RecursiveOpenStruct.new(prioritized_config)
10
10
  end
11
11
 
12
+ def version_hash
13
+ @version_hash ||= Digest::MD5.hexdigest(sensitive_config.to_json)
14
+ end
15
+
12
16
  private
13
17
 
14
18
  attr_reader :runtime_config
@@ -17,10 +21,21 @@ module Photish
17
21
  {}.merge(default_config)
18
22
  .merge(file_config)
19
23
  .merge(runtime_config)
24
+ .merge(derived_config)
25
+ end
26
+
27
+ def sensitive_config
28
+ prioritized_config.slice('qualities')
29
+ end
30
+
31
+ def derived_config
32
+ {
33
+ config_file_location: config_file_location
34
+ }
20
35
  end
21
36
 
22
37
  def file_config
23
- symbolize(FileConfig.new(file_config_location)
38
+ symbolize(FileConfig.new(config_file_location)
24
39
  .hash)
25
40
  end
26
41
 
@@ -28,9 +43,9 @@ module Photish
28
43
  symbolize(DefaultConfig.new.hash)
29
44
  end
30
45
 
31
- def file_config_location
32
- FileConfigLocation.new(runtime_config[:site_dir])
33
- .path
46
+ def config_file_location
47
+ FileConfigLocation.new(runtime_config[:config_dir])
48
+ .path
34
49
  end
35
50
 
36
51
  def symbolize(hash)
@@ -7,7 +7,10 @@ module Photish
7
7
  site_dir: File.join(Dir.pwd, 'site'),
8
8
  photo_dir: File.join(Dir.pwd, 'photos'),
9
9
  output_dir: File.join(Dir.pwd, 'output'),
10
- max_workers: Facter.value('processors')['count'],
10
+ workers: workers,
11
+ threads: threads,
12
+ worker_index: 0,
13
+ photish_executable: photish_executable,
11
14
  qualities: [
12
15
  { name: 'Original',
13
16
  params: [] },
@@ -26,11 +29,34 @@ module Photish
26
29
  level: 'info'
27
30
  },
28
31
  url: {
29
- host: '/',
32
+ host: '',
30
33
  base: nil
31
34
  }
32
35
  }
33
36
  end
37
+
38
+ private
39
+
40
+ def workers
41
+ processor_count / 2
42
+ end
43
+
44
+ def threads
45
+ 2
46
+ end
47
+
48
+ def processor_count
49
+ Facter.value('processors')['count']
50
+ end
51
+
52
+ def photish_executable
53
+ File.join(File.dirname(__FILE__),
54
+ '..',
55
+ '..',
56
+ '..',
57
+ 'exe',
58
+ 'photish')
59
+ end
34
60
  end
35
61
  end
36
62
  end