Initial work on using Liquid::Drops instead of Hashes.

The properties of Liquid::Drops are only evaluated when they're asked for
and therefore save computation time. This prevents a lot of GC time cleaning
up objects that are not needed, because they're not created unless requested.
Additionally, this saves time for actual computation of those values because
they can be computed only if needed.

It's funny how much it helps when you only do what is needed. Far less overhead.
This commit is contained in:
Parker Moore 2015-12-21 22:45:26 -05:00
parent f92d6639e6
commit 82c3ee365f
16 changed files with 303 additions and 101 deletions

View File

@ -172,6 +172,7 @@ end
require_all 'jekyll/commands' require_all 'jekyll/commands'
require_all 'jekyll/converters' require_all 'jekyll/converters'
require_all 'jekyll/converters/markdown' require_all 'jekyll/converters/markdown'
require_all 'jekyll/drops'
require_all 'jekyll/generators' require_all 'jekyll/generators'
require_all 'jekyll/tags' require_all 'jekyll/tags'

View File

@ -170,14 +170,7 @@ module Jekyll
# #
# Returns a representation of this collection for use in Liquid. # Returns a representation of this collection for use in Liquid.
def to_liquid def to_liquid
metadata.merge({ Drops::CollectionDrop.new self
"label" => label,
"docs" => docs,
"files" => files,
"directory" => directory,
"output" => write?,
"relative_directory" => relative_directory
})
end end
# Whether the collection's documents ought to be written as individual # Whether the collection's documents ought to be written as individual

View File

@ -210,13 +210,8 @@ module Jekyll
while layout while layout
Jekyll.logger.debug "Rendering Layout:", path Jekyll.logger.debug "Rendering Layout:", path
payload = Utils.deep_merge_hashes( payload.content = output
payload, payload.layout = layout.data
{
"content" => output,
"layout" => layout.data
}
)
self.output = render_liquid(layout.content, self.output = render_liquid(layout.content,
payload, payload,
@ -250,11 +245,11 @@ module Jekyll
Jekyll.logger.debug "Pre-Render Hooks:", self.relative_path Jekyll.logger.debug "Pre-Render Hooks:", self.relative_path
Jekyll::Hooks.trigger hook_owner, :pre_render, self, payload Jekyll::Hooks.trigger hook_owner, :pre_render, self, payload
info = { :filters => [Jekyll::Filters], :registers => { :site => site, :page => payload['page'] } } info = { :filters => [Jekyll::Filters], :registers => { :site => site, :page => payload.page } }
# render and transform content (this becomes the final content of the object) # render and transform content (this becomes the final content of the object)
payload["highlighter_prefix"] = converters.first.highlighter_prefix payload.highlighter_prefix = converters.first.highlighter_prefix
payload["highlighter_suffix"] = converters.first.highlighter_suffix payload.highlighter_suffix = converters.first.highlighter_suffix
if render_with_liquid? if render_with_liquid?
Jekyll.logger.debug "Rendering Liquid:", self.relative_path Jekyll.logger.debug "Rendering Liquid:", self.relative_path

View File

@ -38,12 +38,10 @@ module Jekyll
end end
def output=(output) def output=(output)
@to_liquid = nil
@output = output @output = output
end end
def content=(content) def content=(content)
@to_liquid = nil
@content = content @content = content
end end
@ -181,27 +179,7 @@ module Jekyll
# #
# Returns the Hash of key-value pairs for replacement in the URL. # Returns the Hash of key-value pairs for replacement in the URL.
def url_placeholders def url_placeholders
{ @url_placeholders ||= Drops::UrlDrop.new(self)
collection: collection.label,
path: cleaned_relative_path,
output_ext: output_ext,
name: Utils.slugify(basename_without_ext),
title: Utils.slugify(data['slug'], mode: "pretty", cased: true) || Utils
.slugify(basename_without_ext, mode: "pretty", cased: true),
slug: Utils.slugify(data['slug']) || Utils.slugify(basename_without_ext),
year: date.strftime("%Y"),
month: date.strftime("%m"),
day: date.strftime("%d"),
hour: date.strftime("%H"),
minute: date.strftime("%M"),
second: date.strftime("%S"),
i_day: date.strftime("%-d"),
i_month: date.strftime("%-m"),
categories: (data['categories'] || []).map { |c| c.to_s.downcase }.uniq.join('/'),
short_month: date.strftime("%b"),
short_year: date.strftime("%y"),
y_day: date.strftime("%j"),
}
end end
# The permalink for this Document. # The permalink for this Document.
@ -278,8 +256,6 @@ module Jekyll
# #
# Returns nothing. # Returns nothing.
def read(opts = {}) def read(opts = {})
@to_liquid = nil
Jekyll.logger.debug "Reading:", relative_path Jekyll.logger.debug "Reading:", relative_path
if yaml_file? if yaml_file?
@ -353,21 +329,7 @@ module Jekyll
# #
# Returns a Hash representing this Document's data. # Returns a Hash representing this Document's data.
def to_liquid def to_liquid
@to_liquid ||= if data.is_a?(Hash) @to_liquid ||= Drops::DocumentDrop.new(self)
Utils.deep_merge_hashes Utils.deep_merge_hashes({
"output" => output,
"content" => content,
"relative_path" => relative_path,
"path" => relative_path,
"url" => url,
"collection" => collection.label,
"next" => next_doc,
"previous" => previous_doc,
"id" => id,
}, data), { 'excerpt' => data['excerpt'].to_s }
else
data
end
end end
# The inspect string for this document. # The inspect string for this document.

View File

@ -0,0 +1,22 @@
# encoding: UTF-8
require "jekyll/drops/immutable_drop"
module Jekyll
module Drops
class CollectionDrop < ImmutableDrop
extend Forwardable
def_delegators :@obj, :label, :docs, :files, :directory, :relative_directory
def output
@obj.write?
end
private
def data
@obj.metadata
end
end
end
end

View File

@ -0,0 +1,50 @@
# encoding: UTF-8
module Jekyll
module Drops
class DocumentDrop < ImmutableDrop
def output
@obj.output
end
def content
@obj.content
end
def relative_path
@obj.relative_path
end
alias_method :path, :relative_path
def url
@obj.url
end
def collection
@obj.collection.label
end
def next
@obj.next_doc
end
def previous
@obj.previous_doc
end
def id
@obj.id
end
def excerpt
data['excerpt'].to_s
end
def data
@obj.data
end
end
end
end

View File

@ -0,0 +1,29 @@
# encoding: UTF-8
module Jekyll
module Drops
class ImmutableDrop < Liquid::Drop
def initialize(obj)
@obj = obj
end
def [](key)
if respond_to? key
public_send key
else
data[key]
end
end
def []=(key, val)
if respond_to? key
raise ArgumentError.new("Key #{key} cannot be set in the drop.")
else
data[key] = val
end
end
end
end
end

View File

@ -0,0 +1,21 @@
# encoding: UTF-8
module Jekyll
module Drops
class JekyllDrop < Liquid::Drop
class << self
def global
@global ||= JekyllDrop.new
end
end
def version
Jekyll::VERSION
end
def environment
Jekyll.env
end
end
end
end

View File

@ -0,0 +1,28 @@
# encoding: UTF-8
module Jekyll
module Drops
class MutableDrop < Liquid::Drop
def initialize(obj)
@obj = obj
@mutations = {}
end
def [](key)
if @mutations.key? key
@mutations[key]
elsif respond_to? key
public_send key
else
data[key]
end
end
def []=(key, val)
@mutations[key] = val
end
end
end
end

View File

@ -0,0 +1,38 @@
# encoding: UTF-8
module Jekyll
module Drops
class SiteDrop < ImmutableDrop
extend Forwardable
def_delegator :@obj, :site_data, :data
def_delegators :@obj, :time, :pages, :static_files, :documents
def posts
@site_posts ||= @obj.posts.docs.sort { |a, b| b <=> a }
end
def html_pages
@site_html_pages ||= @obj.pages.select { |page| page.html? || page.url.end_with?("/") }
end
def categories
@site_categories ||= @obj.post_attr_hash('categories')
end
def tags
@site_tags ||= @obj.post_attr_hash('tags')
end
def collections
@site_collections ||= @obj.collections.values.map(&:to_liquid)
end
private
def data
@obj.config
end
end
end
end

View File

@ -0,0 +1,24 @@
# encoding: UTF-8
module Jekyll
module Drops
class UnifiedPayloadDrop < Liquid::Drop
attr_accessor :page, :layout, :content, :paginator
attr_accessor :highlighter_prefix, :highlighter_suffix
def initialize(site)
@site = site
end
def jekyll
JekyllDrop.global
end
def site
@site_drop ||= SiteDrop.new(@site)
end
end
end
end

View File

@ -0,0 +1,52 @@
# encoding: UTF-8
module Jekyll
module Drops
class UrlDrop < ImmutableDrop
def collection
@obj.collection.label
end
def path
@obj.cleaned_relative_path
end
def output_ext
@obj.output_ext
end
def name
Utils.slugify(@obj.basename_without_ext)
end
def title
Utils.slugify(@obj.data['slug'], mode: "pretty", cased: true) ||
Utils.slugify(@obj.basename_without_ext, mode: "pretty", cased: true)
end
def slug
Utils.slugify(@obj.data['slug']) || Utils.slugify(@obj.basename_without_ext)
end
def categories
category_set = Set.new
Array(@obj.data['categories']).each do |category|
category_set << category.to_s.downcase
end
category_set.to_a.join('/')
end
def year; @obj.date.strftime("%Y"); end
def month; @obj.date.strftime("%m"); end
def day; @obj.date.strftime("%d"); end
def hour; @obj.date.strftime("%H"); end
def minute; @obj.date.strftime("%M"); end
def second; @obj.date.strftime("%S"); end
def i_day; @obj.date.strftime("%-d"); end
def i_month; @obj.date.strftime("%-m"); end
def short_month; @obj.date.strftime("%b"); end
def short_year; @obj.date.strftime("%y"); end
def y_day; @obj.date.strftime("%j"); end
end
end
end

View File

@ -117,12 +117,10 @@ module Jekyll
# #
# Returns nothing. # Returns nothing.
def render(layouts, site_payload) def render(layouts, site_payload)
payload = Utils.deep_merge_hashes({ site_payload.page = to_liquid
"page" => to_liquid, site_payload.paginator = pager.to_liquid
'paginator' => pager.to_liquid
}, site_payload)
do_layout(payload, layouts) do_layout(site_payload, layouts)
end end
# The path to the source file # The path to the source file

View File

@ -3,12 +3,12 @@
module Jekyll module Jekyll
class Renderer class Renderer
attr_reader :document, :site, :site_payload attr_reader :document, :site, :payload
def initialize(site, document, site_payload = nil) def initialize(site, document, site_payload = nil)
@site = site @site = site
@document = document @document = document
@site_payload = site_payload @payload = site_payload || site.site_payload
end end
# Determine which converters to use based on this document's # Determine which converters to use based on this document's
@ -33,12 +33,10 @@ module Jekyll
def run def run
Jekyll.logger.debug "Rendering:", document.relative_path Jekyll.logger.debug "Rendering:", document.relative_path
payload = Utils.deep_merge_hashes({ payload.page = document.to_liquid
"page" => document.to_liquid
}, site_payload || site.site_payload)
if document.collection.label == 'posts' && document.is_a?(Document) if document.collection.label == 'posts' && document.is_a?(Document)
payload['site']['related_posts'] = document.related_posts payload.site['related_posts'] = document.related_posts
end end
Jekyll.logger.debug "Pre-Render Hooks:", document.relative_path Jekyll.logger.debug "Pre-Render Hooks:", document.relative_path
@ -46,12 +44,12 @@ module Jekyll
info = { info = {
filters: [Jekyll::Filters], filters: [Jekyll::Filters],
registers: { :site => site, :page => payload['page'] } registers: { :site => site, :page => payload.page }
} }
# render and transform content (this becomes the final content of the object) # render and transform content (this becomes the final content of the object)
payload["highlighter_prefix"] = converters.first.highlighter_prefix payload.highlighter_prefix = converters.first.highlighter_prefix
payload["highlighter_suffix"] = converters.first.highlighter_suffix payload.highlighter_suffix = converters.first.highlighter_suffix
output = document.content output = document.content
@ -135,14 +133,9 @@ module Jekyll
used = Set.new([layout]) used = Set.new([layout])
while layout while layout
payload = Utils.deep_merge_hashes( payload.content = output
payload, payload.page = document.to_liquid
{ payload.layout = layout.data
"content" => output,
"page" => document.to_liquid,
"layout" => layout.data
}
)
output = render_liquid( output = render_liquid(
layout.content, layout.content,

View File

@ -259,25 +259,7 @@ module Jekyll
# "tags" - The Hash of tag values and Posts. # "tags" - The Hash of tag values and Posts.
# See Site#post_attr_hash for type info. # See Site#post_attr_hash for type info.
def site_payload def site_payload
{ Drops::UnifiedPayloadDrop.new self
"jekyll" => {
"version" => Jekyll::VERSION,
"environment" => Jekyll.env
},
"site" => Utils.deep_merge_hashes(config,
Utils.deep_merge_hashes(Hash[collections.map{|label, coll| [label, coll.docs]}], {
"time" => time,
"posts" => posts.docs.sort { |a, b| b <=> a },
"pages" => pages,
"static_files" => static_files,
"html_pages" => pages.select { |page| page.html? || page.url.end_with?("/") },
"categories" => post_attr_hash('categories'),
"tags" => post_attr_hash('tags'),
"collections" => collections.values.map(&:to_liquid),
"documents" => documents,
"data" => site_data
}))
}
end end
# Get the implementation class for the given Converter. # Get the implementation class for the given Converter.

View File

@ -59,6 +59,14 @@ module Jekyll
# #
# Returns the unsanitized String URL # Returns the unsanitized String URL
def generate_url(template) def generate_url(template)
if @placeholders.is_a? Drops::UrlDrop
generate_url_from_drop(template)
else
generate_url_from_hash(template)
end
end
def generate_url_from_hash(template)
@placeholders.inject(template) do |result, token| @placeholders.inject(template) do |result, token|
break result if result.index(':').nil? break result if result.index(':').nil?
if token.last.nil? if token.last.nil?
@ -70,6 +78,12 @@ module Jekyll
end end
end end
def generate_url_from_drop(template)
template.gsub(/(:[a-z_]+)/) do |match|
@placeholders.public_send(match.sub(':', ''))
end.gsub(/\/\//, '/')
end
# Returns a sanitized String URL, stripping "../../" and multiples of "/", # Returns a sanitized String URL, stripping "../../" and multiples of "/",
# as well as the beginning "/" so we can enforce and ensure it. # as well as the beginning "/" so we can enforce and ensure it.