Commit graph

52 commits

Author SHA1 Message Date
Jean Boussier
547f111b5b Refactor vm_lookup_cc to allow lock-free lookups in RClass.cc_tbl
In multi-ractor mode, the `cc_tbl` mutations use the RCU pattern,
which allow lock-less reads.

Based on the assumption that invalidations and misses should be
increasingly rare as the process ages, locking on modification
isn't a big concern.
2025-08-01 10:42:04 +02:00
Jean Boussier
f2a7e48dea Make RClass.cc_table a managed object
For now this doesn't change anything, but now that the table
is managed by GC, it opens the door to use RCU when in multi-ractor
mode, hence allow unsynchornized reads.
2025-08-01 10:42:04 +02:00
Jean Boussier
6fd108dd6b id_table.c: reduce duplication in managed_id_table methods 2025-07-15 12:10:52 +02:00
Jean Boussier
6b8dcb7c8f shape.c: fix off by one error in shape_tree_mark 2025-06-04 07:59:20 +02:00
Jean Boussier
e9fd44dd72 shape.c: Implement a lock-free version of get_next_shape_internal
Whenever we run into an inline cache miss when we try to set
an ivar, we may need to take the global lock, just to be able to
lookup inside `shape->edges`.

To solve that, when we're in multi-ractor mode, we can treat
the `shape->edges` as immutable. When we need to add a new
edge, we first copy the table, and then replace it with
CAS.

This increases memory allocations, however we expect that
creating new transitions becomes increasingly rare over time.

```ruby
class A
  def initialize(bool)
    @a = 1
    if bool
      @b = 2
    else
      @c = 3
    end
  end

  def test
    @d = 4
  end
end

def bench(iterations)
  i = iterations
  while i > 0
    A.new(true).test
    A.new(false).test
    i -= 1
  end
end

if ARGV.first == "ractor"
  ractors = 8.times.map do
    Ractor.new do
      bench(20_000_000 / 8)
    end
  end
  ractors.each(&:take)
else
  bench(20_000_000)
end
```

The above benchmark takes 27 seconds in Ractor mode on Ruby 3.4,
and only 1.7s with this branch.

Co-Authored-By: Étienne Barrié <etienne.barrie@gmail.com>
2025-06-02 17:49:53 +02:00
Peter Zhu
ea92374bf7 Replace assert with RUBY_ASSERT in id_table.c
assert does not print the bug report, only the file and line number of
the assertion that failed. RUBY_ASSERT prints the full bug report, which
makes it much easier to debug.
2024-02-12 15:07:47 -05:00
Takashi Kokubun
233ddfac54 Stop exporting symbols for MJIT 2023-03-06 21:59:23 -08:00
Takashi Kokubun
5b21e94beb Expand tabs [ci skip]
[Misc #18891]
2022-07-21 09:42:04 -07:00
Peter Zhu
142d5db112 Support ID_TABLE_STOP for replace function
Iteration should top if the replace function returns ID_TABLE_STOP.
2022-01-25 16:51:16 -05:00
Peter Zhu
4d9ad91a35 Rename rb_id_table_foreach_with_replace
Renames rb_id_table_foreach_with_replace to
rb_id_table_foreach_values_with_replace and passes only the value to the
callback. We can use this in GC compaction when we cannot access the
global symbol array.
2022-01-25 16:51:16 -05:00
Nobuyoshi Nakada
6e901939c6
Revert "ID in rb_id_table_foreach_with_replace [Feature #18253]"
This reverts commit 530e485265.
`rb_id_table_foreach_with_replace` is used during GC compaction,
and the global symbols array can have been moved at that time.
2022-01-26 05:57:01 +09:00
Nobuyoshi Nakada
530e485265 ID in rb_id_table_foreach_with_replace [Feature #18253]
Pass the `ID` from `rb_id_table_foreach_with_replace` to callback
functions.
2022-01-24 15:40:47 -08:00
Nobuyoshi Nakada
bad61d34be
Qundef is not for ID 2021-10-13 12:07:57 +09:00
eileencodes
b91b3bc771 Add a cache for class variables
Redo of 34a2acdac7 and
931138b006 which were reverted.

GitHub PR #4340.

This change implements a cache for class variables. Previously there was
no cache for cvars. Cvar access is slow due to needing to travel all the
way up th ancestor tree before returning the cvar value. The deeper the
ancestor tree the slower cvar access will be.

The benefits of the cache are more visible with a higher number of
included modules due to the way Ruby looks up class variables. The
benchmark here includes 26 modules and shows with the cache, this branch
is 6.5x faster when accessing class variables.

```
compare-ruby: ruby 3.1.0dev (2021-03-15T06:22:34Z master 9e5105c) [x86_64-darwin19]
built-ruby: ruby 3.1.0dev (2021-03-15T12:12:44Z add-cache-for-clas.. c6be009) [x86_64-darwin19]

|         |compare-ruby|built-ruby|
|:--------|-----------:|---------:|
|vm_cvar  |      5.681M|   36.980M|
|         |           -|     6.51x|
```

Benchmark.ips calling `ActiveRecord::Base.logger` from within a Rails
application. ActiveRecord::Base.logger has 71 ancestors. The more
ancestors a tree has, the more clear the speed increase. IE if Base had
only one ancestor we'd see no improvement. This benchmark is run on a
vanilla Rails application.

Benchmark code:

```ruby
require "benchmark/ips"
require_relative "config/environment"

Benchmark.ips do |x|
  x.report "logger" do
    ActiveRecord::Base.logger
  end
end
```

Ruby 3.0 master / Rails 6.1:

```
Warming up --------------------------------------
              logger   155.251k i/100ms
Calculating -------------------------------------
```

Ruby 3.0 with cvar cache /  Rails 6.1:

```
Warming up --------------------------------------
              logger     1.546M i/100ms
Calculating -------------------------------------
              logger     14.857M (± 4.8%) i/s -     74.198M in   5.006202s
```

Lastly we ran a benchmark to demonstate the difference between master
and our cache when the number of modules increases. This benchmark
measures 1 ancestor, 30 ancestors, and 100 ancestors.

Ruby 3.0 master:

```
Warming up --------------------------------------
            1 module     1.231M i/100ms
          30 modules   432.020k i/100ms
         100 modules   145.399k i/100ms
Calculating -------------------------------------
            1 module     12.210M (± 2.1%) i/s -     61.553M in   5.043400s
          30 modules      4.354M (± 2.7%) i/s -     22.033M in   5.063839s
         100 modules      1.434M (± 2.9%) i/s -      7.270M in   5.072531s

Comparison:
            1 module: 12209958.3 i/s
          30 modules:  4354217.8 i/s - 2.80x  (± 0.00) slower
         100 modules:  1434447.3 i/s - 8.51x  (± 0.00) slower
```

Ruby 3.0 with cvar cache:

```
Warming up --------------------------------------
            1 module     1.641M i/100ms
          30 modules     1.655M i/100ms
         100 modules     1.620M i/100ms
Calculating -------------------------------------
            1 module     16.279M (± 3.8%) i/s -     82.038M in   5.046923s
          30 modules     15.891M (± 3.9%) i/s -     79.459M in   5.007958s
         100 modules     16.087M (± 3.6%) i/s -     81.005M in   5.041931s

Comparison:
            1 module: 16279458.0 i/s
         100 modules: 16087484.6 i/s - same-ish: difference falls within error
          30 modules: 15891406.2 i/s - same-ish: difference falls within error
```

Co-authored-by: Aaron Patterson <tenderlove@ruby-lang.org>
2021-06-18 10:02:44 -07:00
Aaron Patterson
07f055bb13
Revert "Filling cache values on cvar write"
This reverts commit 08de37f9fa.
This reverts commit e8ae922b62.
2021-05-11 13:31:00 -07:00
eileencodes
e8ae922b62 Add a cache for class variables
This change implements a cache for class variables. Previously there was
no cache for cvars. Cvar access is slow due to needing to travel all the
way up th ancestor tree before returning the cvar value. The deeper the
ancestor tree the slower cvar access will be.

The benefits of the cache are more visible with a higher number of
included modules due to the way Ruby looks up class variables. The
benchmark here includes 26 modules and shows with the cache, this branch
is 6.5x faster when accessing class variables.

```
compare-ruby: ruby 3.1.0dev (2021-03-15T06:22:34Z master 9e5105ca45) [x86_64-darwin19]
built-ruby: ruby 3.1.0dev (2021-03-15T12:12:44Z add-cache-for-clas.. c6be0093ae) [x86_64-darwin19]

|         |compare-ruby|built-ruby|
|:--------|-----------:|---------:|
|vm_cvar  |      5.681M|   36.980M|
|         |           -|     6.51x|
```

Benchmark.ips calling `ActiveRecord::Base.logger` from within a Rails
application. ActiveRecord::Base.logger has 71 ancestors. The more
ancestors a tree has, the more clear the speed increase. IE if Base had
only one ancestor we'd see no improvement. This benchmark is run on a
vanilla Rails application.

Benchmark code:

```ruby
require "benchmark/ips"
require_relative "config/environment"

Benchmark.ips do |x|
  x.report "logger" do
    ActiveRecord::Base.logger
  end
end
```

Ruby 3.0 master / Rails 6.1:

```
Warming up --------------------------------------
              logger   155.251k i/100ms
Calculating -------------------------------------
```

Ruby 3.0 with cvar cache /  Rails 6.1:

```
Warming up --------------------------------------
              logger     1.546M i/100ms
Calculating -------------------------------------
              logger     14.857M (± 4.8%) i/s -     74.198M in   5.006202s
```

Lastly we ran a benchmark to demonstate the difference between master
and our cache when the number of modules increases. This benchmark
measures 1 ancestor, 30 ancestors, and 100 ancestors.

Ruby 3.0 master:

```
Warming up --------------------------------------
            1 module     1.231M i/100ms
          30 modules   432.020k i/100ms
         100 modules   145.399k i/100ms
Calculating -------------------------------------
            1 module     12.210M (± 2.1%) i/s -     61.553M in   5.043400s
          30 modules      4.354M (± 2.7%) i/s -     22.033M in   5.063839s
         100 modules      1.434M (± 2.9%) i/s -      7.270M in   5.072531s

Comparison:
            1 module: 12209958.3 i/s
          30 modules:  4354217.8 i/s - 2.80x  (± 0.00) slower
         100 modules:  1434447.3 i/s - 8.51x  (± 0.00) slower
```

Ruby 3.0 with cvar cache:

```
Warming up --------------------------------------
            1 module     1.641M i/100ms
          30 modules     1.655M i/100ms
         100 modules     1.620M i/100ms
Calculating -------------------------------------
            1 module     16.279M (± 3.8%) i/s -     82.038M in   5.046923s
          30 modules     15.891M (± 3.9%) i/s -     79.459M in   5.007958s
         100 modules     16.087M (± 3.6%) i/s -     81.005M in   5.041931s

Comparison:
            1 module: 16279458.0 i/s
         100 modules: 16087484.6 i/s - same-ish: difference falls within error
          30 modules: 15891406.2 i/s - same-ish: difference falls within error
```

Co-authored-by: Aaron Patterson <tenderlove@ruby-lang.org>
2021-05-11 12:04:27 -07:00
Nobuyoshi Nakada
187c164d71
Suppress unused-variable warning
`key` is not used outside this assertion.
2020-08-10 17:49:08 +09:00
卜部昌平
32623150cd reroute redefinition of NDEBUG
NDEBUG can be defined via a command-line argument.  Should take care of
such situations.
2020-04-21 13:07:19 +09:00
Koichi Sasada
b9007b6c54 Introduce disposable call-cache.
This patch contains several ideas:

(1) Disposable inline method cache (IMC) for race-free inline method cache
    * Making call-cache (CC) as a RVALUE (GC target object) and allocate new
      CC on cache miss.
    * This technique allows race-free access from parallel processing
      elements like RCU.
(2) Introduce per-Class method cache (pCMC)
    * Instead of fixed-size global method cache (GMC), pCMC allows flexible
      cache size.
    * Caching CCs reduces CC allocation and allow sharing CC's fast-path
      between same call-info (CI) call-sites.
(3) Invalidate an inline method cache by invalidating corresponding method
    entries (MEs)
    * Instead of using class serials, we set "invalidated" flag for method
      entry itself to represent cache invalidation.
    * Compare with using class serials, the impact of method modification
      (add/overwrite/delete) is small.
    * Updating class serials invalidate all method caches of the class and
      sub-classes.
    * Proposed approach only invalidate the method cache of only one ME.

See [Feature #16614] for more details.
2020-02-22 09:58:59 +09:00
卜部昌平
eb92159d72 Revert https://github.com/ruby/ruby/pull/2486
This reverts commits: 10d6a3aca7 8ba48c1b85 fba8627dc1 dd883de5ba
6c6a25feca 167e6b48f1 7cb96d41a5 3207979278 595b3c4fdd 1521f7cf89
c11c5e69ac cf33608203 3632a812c0 f56506be0d 86427a3219 .

The reason for the revert is that we observe ABA problem around
inline method cache.  When a cache misshits, we search for a
method entry.  And if the entry is identical to what was cached
before, we reuse the cache.  But the commits we are reverting here
introduced situations where a method entry is freed, then the
identical memory region is used for another method entry.  An
inline method cache cannot detect that ABA.

Here is a code that reproduce such situation:

```ruby
require 'prime'

class << Integer
  alias org_sqrt sqrt
  def sqrt(n)
    raise
  end

  GC.stress = true
  Prime.each(7*37){} rescue nil # <- Here we populate CC
  class << Object.new; end

  # These adjacent remove-then-alias maneuver
  # frees a method entry, then immediately
  # reuses it for another.
  remove_method :sqrt
  alias sqrt org_sqrt
end

Prime.each(7*37).to_a # <- SEGV
```
2019-10-03 12:45:24 +09:00
卜部昌平
3632a812c0 refactor add rb_id_table_foreach_with_replace_with_key
This is a pure refactoring to reduce copy & paste.  Also the new
function is made visible from other parts of the interpreter, to
be used later.
2019-09-30 10:26:38 +09:00
tenderlove
91793b8967 Add GC.compact again.
🙏

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67620 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2019-04-20 01:19:47 +00:00
tenderlove
744e5df715 Reverting compaction for now
For some reason symbols (or classes) are being overridden in trunk

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67598 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2019-04-17 09:41:41 +00:00
tenderlove
3c55b643ae Adding GC.compact and compacting GC support.
This commit adds the new method `GC.compact` and compacting GC support.
Please see this issue for caveats:

  https://bugs.ruby-lang.org/issues/15626

[Feature #15626]

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67576 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2019-04-17 03:17:25 +00:00
kazu
25c1fd3b90 Reverting all commits from r67479 to r67496 because of CI failures
Because hard to specify commits related to r67479 only.
So please commit again.


git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67499 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2019-04-10 09:15:21 +00:00
nobu
b8e3a4bd2c id_table.c: use NULL as ID* instead of Qundef
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67495 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2019-04-10 06:44:43 +00:00
nobu
5ae753dfa3 Adjusted styles
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67493 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2019-04-10 06:44:41 +00:00
tenderlove
3ef4db15e9 Adding GC.compact and compacting GC support.
This commit adds the new method `GC.compact` and compacting GC support.
Please see this issue for caveats:

  https://bugs.ruby-lang.org/issues/15626

[Feature #15626]

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67479 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2019-04-09 20:32:04 +00:00
nobu
6924066a65 adjust styles [ci skip]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@58646 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2017-05-10 00:39:26 +00:00
ko1
4c2d014e92 swithc id_table data structure.
* id_table.c: swtich to "simple open addressing with quadratic probing"
  by Yura Sokolov.  For more detail measurements, see [Feature #12180]
* id_table.c: remove other algorithms to simplify the source code.


git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@57416 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2017-01-25 03:03:52 +00:00
nobu
00ed779cb0 id_table.c: fix typo
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@57369 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2017-01-19 05:24:34 +00:00
nobu
dfe1d4fda6 id_table.c: extend, don't shrink
* id_table.c (hash_table_extend): should not shrink the table than
  the previous capacity.  [ruby-core:76534] [Bug #12614]

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@55896 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2016-08-14 13:35:42 +00:00
nobu
a62dcd947c id_table.h: dummy sentinel
* id_table.h (rb_id_table_iterator_result): add dummy sentinel
  member because C standard prohibits a trailing comma.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@55821 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2016-08-06 00:35:33 +00:00
ngoto
02cafdf491 capa should be even number on 64-bit SPARC for 8-byte word alignment
* id_table.c (list_id_table_init): When unaligned word access is
  prohibited and sizeof(VALUE) is 8 (64-bit machines),
  capa should always be even number for 8-byte word alignment
  of the values of a table. This code assumes that sizeof(ID) is 4,
  sizeof(VALUE) is 8, and xmalloc() returns 8-byte aligned memory.
  This fixes bus error on 64-bit SPARC Solaris 10.
  [Bug #12406][ruby-dev:49631]


git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@55086 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2016-05-20 14:55:23 +00:00
nobu
439224a590 RUBY_ASSERT
* error.c (rb_assert_failure): assertion with stack dump.
* ruby_assert.h (RUBY_ASSERT): new header for the assertion.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@53615 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2016-01-22 08:33:55 +00:00
normal
b147b5a3b8 id_table: const correctness for _size and _memsize
This allows us to swap in rb_id_table_memsize for st_memsize
(which takes a "const st_table *") more easily.

It also makes sense to do the same for rb_id_table_size,
too; as the table cannot be altered when accessing size.

* id_table.h (rb_id_table_size): const arg
  (rb_id_table_memsize): ditto
* id_table.c (st_id_table_size): ditto
  (st_id_table_memsize): ditto
  (list_id_table_size): ditto
  (list_id_table_memsize): ditto
  (hash_id_table_size): ditto
  (hash_id_table_memsize): ditto
  (mix_id_table_size): ditto
  (mix_id_table_memsize): ditto

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@52428 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-11-02 22:18:32 +00:00
ko1
840e6b6307 * id_table.c (mix_id_table_insert): do not touch list during
list->hash transition because GC can run during transition.




git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@52421 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-11-01 08:17:25 +00:00
nobu
7f13f878cf id_table.c: fix prototype names
* id_table.c: fix prototype names, missing underscore prefixes.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51950 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-09-27 00:23:18 +00:00
nobu
33ed298379 id_table.c: fix prototype names
* id_table.c: fix prototype names, missing underscore prefixes.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51949 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-09-27 00:17:53 +00:00
nobu
6b64ffd225 id_table.c: suppress warnings
* id_table.c (UNUSED): mark implementation functions maybe-unused
  to suppress warnings by old gcc.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51948 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-09-27 00:10:47 +00:00
hsbt
1bbe442f80 * id_table.c: fix typo. [ci skip][fix GH-1031] Patch @davydovanton
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51932 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-09-25 00:47:17 +00:00
nobu
80e386e92c id_table.c: fix types
* id_table.c (insert_into_chain, insert_into_main): fix argument
  types in prototype declarations.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51884 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-09-17 01:01:39 +00:00
nobu
a3b9b9870e id_table.c: aliases
* id_table.c (IMPL_TYPE, IMPL_VOID): make aliases if supported on
  the platform.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51683 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-26 01:20:31 +00:00
nobu
38cfb957dd id_table.h: callback function types
* id_table.h (rb_id_table_foreach_func_t): define callback
  function type for rb_id_table_foreach().

* id_table.h (rb_id_table_foreach_values_func_t): ditto for
  rb_id_table_foreach_values().

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51682 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-26 00:47:10 +00:00
nobu
86fd28d11e id_table.c: adjust indent
* id_table.c (list_table_extend, fix_empty): adjust indent.
  (hash_id_table_foreach_values): ditto.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51681 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-26 00:46:58 +00:00
nobu
f92ed1943c id_table.c: constify
* id_table.c (find_empty): constify static data.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51680 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-26 00:46:34 +00:00
nobu
341c84ed22 id_table.c: fix for C89 compilers
* id_table.c (list_table_extend, hash_table_extend): remove C99
  features.  [ruby-dev:49239] [Bug #11487]

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51678 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-26 00:26:02 +00:00
nobu
235e7738d6 id_table.c: TOKEN_PASTE
* id_table.c (IMPL1): use TOKEN_PASTE, and prevent `op' from
  expansion.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51562 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-13 04:41:54 +00:00
nobu
2c3b813bd3 id_table.c: prefix first
* id_table.c (IMPL): prepend id_table to the argument before its
  expansion.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51550 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-12 13:20:21 +00:00
ko1
1761312577 * id_table.c: IMPL() macro accept op as _opname instead of opname
because jemalloc seems to replace the word `free' to `je_free'.



git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@51549 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2015-08-12 12:59:29 +00:00