photish 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
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