434 Commits

Author SHA1 Message Date
Valentin Shergin
cdb983d339 Fabric: Lock-free events 4/n: Added an assert in EventTarget
Summary: See the diff for more details.

Reviewed By: sahrens

Differential Revision: D13642593

fbshipit-source-id: 8bdcc91bcc2ea1e4093bcac03d87167b8901cbb4
2019-01-16 20:22:39 -08:00
Valentin Shergin
97f9e4082c Fabric: Lock-free events 3/n: EventEmitter::DispatchMutex is not recursive mutex anymore
Summary: The mutex is a "leaf" mutex, so the code cannot reenter that, so there is no reason to be a recursive one.

Reviewed By: sahrens

Differential Revision: D13642592

fbshipit-source-id: 0fb64d3405c5d3408251dc983c186f6747bc6ee2
2019-01-16 20:22:39 -08:00
Valentin Shergin
b1814b37aa Fabric: Lock-free events 2/n: Reimagining of EventTarget
Summary:
EventTargetWrapper and EventTarget were merged into one class that controls an `instanceHandle` reference and extracting a strong reference to it.

This diff also decouples the operation of retaining a strong reference (with checking a `enabled` flag) from actual usage of this reference. This allows to wrap into a mutex only first part of this process and avoid possible deadlocks.

Reviewed By: sahrens

Differential Revision: D13616382

fbshipit-source-id: 9907bc12047386fcf027929ae2ae41c0b727cd06
2019-01-16 20:22:39 -08:00
Valentin Shergin
bea80b2276 Fabric: Lock-free events 1/n: Removing WeakEventTarget trick from EventEmitter
Summary:
For some time we have had a nit trick inside `EventEmitter`: When event emitter is disabled, we switch to store `EventTarget` as a weak pointer instead of complete deleting it. Given some unpredictability of JS GC, it gave us some time to restore the pointer in case if we increment the counter right after decrementing this. Apparently, we found that it doesn't always work (obviously, sigh...), so we implement the `enableCounter_` and left this feature as it is assuming that it can nice optimization that illuminates some unnecessary constraints.
But, apparently, it's actually harmful, assuming that `jsi::WeakObject` (that thing that we use to refer to actual JS target) actually has "unsafe unretained" (not "weak") semantic.
So, we have to remove this.
This change will be particularly important for coming diffs in the stack.

Reviewed By: mdvacca

Differential Revision: D13324480

fbshipit-source-id: 4c4da2984dc6a36b94b564bc9eee144142b430b0
2019-01-16 20:22:39 -08:00
Valentin Shergin
9f71a8f26e Fabric: ShadowTree::getRootShadowNode was removed
Summary: Because it was not idiomatic anyway. There is no point having a pointer to RootShadowNode without any guarantees that it's the current one. Using `commit` method makes this concern explicit.

Reviewed By: sahrens

Differential Revision: D13615363

fbshipit-source-id: f71ffc3c55dbdc69624933eb8b92334ed793c794
2019-01-16 20:22:39 -08:00
Valentin Shergin
84cf657308 Fabric: ShadowTree::completeByReplacingShadowNode was moved to RootShadowNode
Summary:
Removing additional complexity from ShadowTree should help with maintainability. Now, this class is "tricky", but short at least.
With new `commit` API, it's much more simple and expected this way.

Reviewed By: sahrens

Differential Revision: D13615365

fbshipit-source-id: 1fe851c1a2d3bdc7ac2f4a570cf0170eae3c4c67
2019-01-16 20:22:39 -08:00
Valentin Shergin
c937300f5d Fabric: Layout-related methods were removed from ShadowTree
Summary: Now it's parts of RootShadowNode and Scheduler.

Reviewed By: sahrens

Differential Revision: D13615364

fbshipit-source-id: 13dbea1e69ef51b2679101915c01c6be7e15d859
2019-01-16 20:22:39 -08:00
Valentin Shergin
5a58ca4144 Fabric: New non-blocking treading model for ShadowTree
Summary:
Instead of the whole family of commit* and complete* methods, now we have one single `commit` method which performs pre- and post-commit operations and swap pointers in a thread-safe manner. The `commit` operation is also exposing `revision` number and allows perform multiple commit attempts.

`completeByReplacingShadowNode`, `measure` and `constraintLayout` are also going away to RootShadowNode class in the next commits.

Why?
* Nicer API;
* No more recursive_mutex, no more problems with thread jumps;
* All mutex locks are now leaf-locks, so no more deadlocks possible;
* Exposing `revision` should help with debugging races.

Reviewed By: sahrens

Differential Revision: D13613942

fbshipit-source-id: 94e797d2f7860717847e823b5d97c4f7b35f08df
2019-01-16 20:22:39 -08:00
Valentin Shergin
8f9ca2b9a0 Fabric: Even more systraces
Summary: Trivial.

Reviewed By: mdvacca

Differential Revision: D13664395

fbshipit-source-id: 3de5d65d6fcf8b68bce2636fc91492defdbe8405
2019-01-16 20:22:39 -08:00
David Vacca
b421b5f4bd Open source Fabric android
Summary: This diff open sources Fabric Android implementation and it extracts ComponentDescriptorFactory into a function that can be "injected" per application

Reviewed By: shergin

Differential Revision: D13616172

fbshipit-source-id: 7b7a6461216740b5a1ad5ebbead9e37de4570221
2019-01-16 12:38:22 -08:00
Eli White
db5528ffa9 Use Generated Switch Schema
Summary: We are now generating the native cpp files for Switch via Buck. Deleting the hand written files and switching over.

Reviewed By: JoshuaGross, mdvacca

Differential Revision: D13666672

fbshipit-source-id: 72cf6f6af9374511f2742f8f0d996fa52e1bff5b
2019-01-15 18:10:34 -08:00
Valentin Shergin
cb14b06309 Fabric: More systraces for Diffing and Commit phase
Summary: Trivial.

Reviewed By: mdvacca

Differential Revision: D13644482

fbshipit-source-id: 90cfa04797682e57fc4de009fe4a412167b9e8cd
2019-01-11 18:13:55 -08:00
Valentin Shergin
7bf6f2c238 Fabric: Layout/Yoga related systrace sections
Summary: Trivial.

Reviewed By: mdvacca

Differential Revision: D13643275

fbshipit-source-id: 67a562b899f077d7ca53b64456807a8d7c8fe6b7
2019-01-11 18:13:55 -08:00
David Aurelio
7406276a5b Remove enum count macros
Summary:
@public
Removes all `YG...Count` macros for enums and replaces them with `facebook::yoga::enums::count<YG...>()`.
This removes the need to manually maintain enum counts.

Same as D13597449, working around a defect in clang < 3.9

Reviewed By: amir-shalem

Differential Revision: D13634622

fbshipit-source-id: 344dc70e167b0caf746fe396cedd200f54e52219
2019-01-11 03:11:58 -08:00
Amir Shalem
1feda5e5c8 Revert D13597449: [Yoga][cleanup] Remove enum count macros
Differential Revision:
D13597449

Original commit changeset: edcee225ada4

fbshipit-source-id: 1afc24833c8657a8a198bc4529d98c8f605b7fbd
2019-01-10 23:51:29 -08:00
David Aurelio
35a136801c Remove enum count macros
Summary:
@public
Removes all `YG...Count` macros for enums and replaces them with `facebook::yoga::enums::count<YG...>()`.
This removes the need to manually maintain enum counts.

Reviewed By: shergin

Differential Revision: D13597449

fbshipit-source-id: edcee225ada4058e94f3a727246763e3cc45873d
2019-01-10 07:56:15 -08:00
David Vacca
9380ec0d23 Fix 'global reference table overflow' error in Fabric
Summary: This diff ensures that the 'measure' method in TextLayoutManager is memoized using a static variable.

Reviewed By: fkgozali

Differential Revision: D13585508

fbshipit-source-id: 9275a4d193b8abb0c3aaffd5a5535234717475e1
2019-01-04 16:36:32 -08:00
David Vacca
79cc1468bc Fix race condition in Fabric Scheduler
Summary:
This diff fixes a race condition that was detected on "Marketplace You" production test for Android.
The race condition happens when the method ShadowTree::complete is executed concurrently from two threads (in this case this is called from Scheduller::constraintSurfaceLayout and Scheduler::uiManagerDidFinishTransaction), based on the order of execution this bug makes MountViewItems to be dispatched to the UI in the wrong order.

The root cause of the bug is in the method:

```
bool ShadowTree::complete(
    const SharedRootShadowNode &oldRootShadowNode,
    const UnsharedRootShadowNode &newRootShadowNode) const {
  newRootShadowNode->layout();
  newRootShadowNode->sealRecursive();

  auto mutations =
      calculateShadowViewMutations(*oldRootShadowNode, *newRootShadowNode);

  if (!commit(oldRootShadowNode, newRootShadowNode, mutations)) {
    return false;
  }

  emitLayoutEvents(mutations);

  if (delegate_) {
    delegate_->shadowTreeDidCommit(*this, mutations);
  }

  return true;
}
```
Notes:
- the commit method is guarded by the commitMutex_
- the shadowTreeDidCommit() method dispatches mutations instructions to the platform side.
- If there are two threads running concurrently, there is no guarantee that the shadowTreeDidCommit() is going to be called in the correct order.

The solution is to include the execution to shadowTreeDidCommit() in the same commitMutex_

Possible solutions:
1 - move the commitMutex_ out of the commit method (to the completeMethod)
2 - synchronize the call to complete method() - this is the implemented solution.
I chose this solution to make it consistent with the way Scheduler::constraintSurfaceLayout is implemented (https://fburl.com/8l49no5x)

This mechanism is very likely to change in the refactor of threading mechanism that Valentin Shergin is going to be working on January.

I would like to land this, so we can fix this bug and run another experiment in production as soon as possible.

Reviewed By: sahrens

Differential Revision: D13535587

fbshipit-source-id: bedd4d85f5569ab3733c302d1328aa48017bcaad
2018-12-26 11:15:34 -08:00
Spencer Ahrens
34ea65e3d9 tighter ContextContainer semantics
Summary:
shergin mentioned that he'd like to move away from RTTI a bit and use explicit key strings for context container instances rather than relying on the `typeid`, so this does this.

We also fatal with a useful error message if we get a collision, rather than failing silently.

Reviewed By: shergin

Differential Revision: D13384308

fbshipit-source-id: 0b06d7555b082be89e8f130c23e94be99749a7a3
2018-12-21 18:00:36 -08:00
Spencer Ahrens
089dba3842 expose contextContainer as application API
Summary: We need a way for different apps to inject dependencies or additional functionality into Fabric - ReactNativeConfig might be a special case, but I think this could clean up it's integration nicely, and I'm using this for a uitemplate cache system so we can use CompactDisk or other storage systems for caching depending on the app.

Reviewed By: mdvacca

Differential Revision: D13407287

fbshipit-source-id: 45481908434e6235850aa4d2d6b2bfb936a23be7
2018-12-21 18:00:35 -08:00
David Aurelio
375311091c Decouple from storage type in YGStyle
Summary:
@public

The storage format of `YGValue` in `YGStyle` is an implementation detail that is going to change soon. It is only guaranteed to be assignable from, and castable to `YGValue`.
Here, we remove tight coupling from the actual implementation in React Native.

Reviewed By: shergin

Differential Revision: D13465113

fbshipit-source-id: 41dfcb90c2a1cd825a6732854bf84d4c3318d835
2018-12-18 02:47:10 -08:00
David Aurelio
0eeb94e948 Remove cast from detail::Values to std::array<YGValue
Summary:
@public

When switching to `CompactValue`, casting edges or dimensions to `std::array<YGValue, ...>` will do actual work.
In order to avoid that from happening implicitely, we remove the casting operator.

Reviewed By: SidharthGuglani

Differential Revision: D13464292

fbshipit-source-id: 217065b001a63cfa8adde715063682c583007a4d
2018-12-14 09:23:13 -08:00
Rick Hanlon
e7cf870e31 Fix textDecorationLine property
Summary:
This diff fixes a style property that was incorrectly mapped as `textDecorationLineType` in Fabric

This was correctly mapped in classic here: diffusion/FBS/browse/master/xplat/js/react-native-github/Libraries/Text/BaseText/RCTBaseTextViewManager.m;10b92f1847cdec8a3f0a996f218989766516f805$48

Reviewed By: mdvacca

Differential Revision: D13443921

fbshipit-source-id: 7fafaf2492d8c3b938f2e433a983303958e5c578
2018-12-13 14:02:15 -08:00
David Aurelio
ada4831580 Eliminate YGFloatOptional::getValue()
Summary:
@public

Replace `YGFloatOptional::getValue()` with `YGFloatOptional::unwrap()`.

`YGFloatOptional::getValue()` has the unfortunate property of calling `std::exit` if the wrapped value is undefined.

Here, we eliminate the method, and just call `.unwrap()` everywhere.

Reviewed By: shergin

Differential Revision: D13439608

fbshipit-source-id: 5ae82b170537d0a10c301412567a7a66fd50bab4
2018-12-13 07:16:45 -08:00
Taras Tsugrii
7fb02889d7 Buildify xplat build files.
Summary: drop-conflicts

Differential Revision: D13378356

fbshipit-source-id: 555bcfb30139b165e32e2045b291249c6b6b8650
2018-12-08 13:18:23 -08:00
David Aurelio
90f582ffb9 Back out Stack D13119110..D13236159
Summary: backout, causes failures

Reviewed By: adityasharat

Differential Revision: D13376210

fbshipit-source-id: 1fa8823f2dce601c47738f34ddb2674288197e79
2018-12-07 13:01:28 -08:00
Doug Russell
ee7c702308 Accessibility Escape
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/22047

Differential Revision: D13146179

Pulled By: cpojer

fbshipit-source-id: b8a089114a5deafee47dd482e484d413c8c39137
2018-12-06 19:44:21 -08:00
David Aurelio
4157a49d8d Eliminate YGFloatOptional::getValue()
Summary:
@public

`YGFloatOptional::getValue()` has the unfortunate property of calling `std::exit` if the wrapped value is undefined.
That forces `x.isUndefined() ? fallback : x.getValue()` as access pattern.

Here, we replace that by introducing `YGFloatOptional::orElse(float)` which encapsulates that pattern. Other additions are `orElseGet([] { … })` and some extra operators.

Reviewed By: SidharthGuglani

Differential Revision: D13209152

fbshipit-source-id: 4e5deceaaaaf8eaed44846a8c152cc8b235e815c
2018-12-06 07:38:43 -08:00
Kevin Gozali
01d7aad548 Guard calls to debug features in fabric test
Summary:
In some setup, buck cxx test for android runs with `NDEBUG` set, hence we can't call debug symbols in the test cases. So guard those callsites with `#ifndef NDEBUG`.

Also, some dependencies for this test target depend on Android specific symbols, so we have to mark it as instrumentation test for now (FB-specific).

Reviewed By: sahrens

Differential Revision: D13337637

fbshipit-source-id: 02ff152df9937f2b0b8596f53789cdee8ee8a539
2018-12-05 15:03:27 -08:00
Kevin Gozali
4f9a3bc8f6 xplat: pass through config object to fabric UIManager layer
Summary: For configuration purpose, pass down config object from the hosting app and use it in UITemplateProcessor.

Reviewed By: sahrens

Differential Revision: D13290322

fbshipit-source-id: 8bb6d7f5a3f977b873e548e15603259876b46dc8
2018-12-05 15:03:27 -08:00
Valentin Shergin
757518edba Fabric: ShadowTreeRegistry::get was renamed to ShadowTreeRegistry::visit
Summary: Trivial. The name `visit` expresss intent more clearly and fits C++17 `std::visit` pattern.

Reviewed By: mdvacca

Differential Revision: D13269744

fbshipit-source-id: 6aa6569c58aa66673e3f3a1662390a74290b8e01
2018-11-30 17:29:31 -08:00
Valentin Shergin
d150cbc298 Fabric: Using RW-like shared lock in ShadowTreeRegistry
Summary:
`ShadowTree` class is already thread-safe, so we don't need to (we should not) guard concurent access to it.
We should guard concurrent write-access to the collection of them though.
Note that unordered_map is "thread-compatible" collection, so concurrent reading access is okay.

Reviewed By: mdvacca

Differential Revision: D13269745

fbshipit-source-id: 4779626018da0e42b81a835e538f6c1d1a8e25f7
2018-11-30 17:29:31 -08:00
David Vacca
3c50d04cf2 Cleanup measure function
Summary: Trivial diff that cleans up measure function in C++ and Android

Reviewed By: shergin

Differential Revision: D13200340

fbshipit-source-id: 6c0888439640241cdedf514898a1ba3dac231d6a
2018-11-30 17:19:11 -08:00
Valentin Shergin
c30c803b89 Fabric: Migrating TouchEventEmitter to JSI-based payload
Summary: Pretty straight-forward migration to using `JSI` instead of `folly::dynamic` in `TouchEventEmitter`.

Reviewed By: sahrens

Differential Revision: D13123042

fbshipit-source-id: 594b89b6e3986d6a04846194701e3a727b152cec
2018-11-27 21:00:56 -08:00
Valentin Shergin
847e6fdd99 Fabric: Migrating ScrollViewEventEmitter to JSI-based payload
Summary: Pretty straight-forward migration to using `JSI` instead of `folly::dynamic` in `ScrollViewEventEmitter`.

Reviewed By: sahrens

Differential Revision: D13123049

fbshipit-source-id: 2839976d0119c48fa2538dbaa53afbc24982c598
2018-11-27 21:00:56 -08:00
Valentin Shergin
610dac4461 Fabric: Migrating ViewEventEmitter to JSI-based payload
Summary: Pretty straight-forward migration to using `JSI` instead of `folly::dynamic` in ViewEventEmitter.

Reviewed By: sahrens

Differential Revision: D13123048

fbshipit-source-id: 3c323912d3e65b684f99df6cda99c785876164af
2018-11-27 21:00:56 -08:00
Valentin Shergin
eeaf0096b1 Fabric: Migrating SwitchEventEmitter to JSI-based payload
Summary: Pretty straight-forward migration to using `JSI` instead of `folly::dynamic` in SwitchEventEmitter.

Reviewed By: sahrens

Differential Revision: D13123046

fbshipit-source-id: f2e4905a96191540ceec633bae1871c93be724db
2018-11-27 21:00:56 -08:00
Valentin Shergin
dea8773c19 Fabric: ValueFactory and EventEmitter::dispatchEvent based on that
Summary:
Now the event delive pipeline supports `JSI::Value`-based payload. Instead of passing `folly::dynamic`, now we are passing `std::function<jsi::Value(jsi::Runtime &runtime)>` as factory that can build a `JSI::Value` with given `jsi::Runtime` and any captured data.
The old (now legacy) way of calling `EventEmitter::dispatchEvent(..., const folly::dynamic &payload, ...)` is also supported.

Reviewed By: sahrens

Differential Revision: D13123043

fbshipit-source-id: d65348bb215013042abb2fcfe5083a8c697333d0
2018-11-27 21:00:55 -08:00
Valentin Shergin
f4da8769a3 Fabric: Coupling Tag inside TargetEventEmitter
Summary: Now instead of passing `reactTag` through the whole event pipeline, we store it inside `EventTargetWrapper` (and it does not leave `UIManagerBinding`). It helps with reducing the complexity of `EventEmitter` and will help us in migrating to JSI.

Reviewed By: sahrens

Differential Revision: D13123045

fbshipit-source-id: aa9ee94d5660ff3090369c1e55cf748d2e72b987
2018-11-27 21:00:55 -08:00
Valentin Shergin
106de1201b Fabric: Fixed incorrect systrace marker in UIManagerBinding::dispatchEvent
Summary: `SystraceSection` relies on RAII, so previously it measured only `if (eventTarget) {`'s body.

Reviewed By: mdvacca

Differential Revision: D13123047

fbshipit-source-id: 463fba783763f694b51325f98314d36b7da2f887
2018-11-27 21:00:55 -08:00
Valentin Shergin
7197aa026b Fabric: Using ShadowView instad of ShadowNode inside AttributedString
Summary:
That's generally better because:
 * Avoids exposing ShadowNode to mounting layer;
 * Enables hashing and comparing the AttributedString based on actual meaningful data (not on just a pointer to ShadowNode).

Reviewed By: mdvacca

Differential Revision: D13205230

fbshipit-source-id: 7b79c1aad97b10d81e3faa10408be61b74f815cf
2018-11-27 18:34:15 -08:00
Valentin Shergin
6c3b05f343 Fabric: std::hash for ShadowView
Summary: Trivial. We need this for future use as part of AttributedString's hash.

Reviewed By: mdvacca

Differential Revision: D13205231

fbshipit-source-id: 14a3decae72741030284a30abdb936616bafb3fe
2018-11-27 18:34:14 -08:00
Valentin Shergin
62173a1569 Introducing fabric/mounting module
Summary:
ShadowView, ShadowViewMutation, and Differentiator were decoupled to separate module.
That enables us to use ShadowView more widely without facing a circular dependency problem.

Reviewed By: mdvacca

Differential Revision: D13205229

fbshipit-source-id: 7373864bf153a7813c2f97edb263a41454ce0b88
2018-11-27 18:34:14 -08:00
Valentin Shergin
28c3981f73 Fabric: UIManagerBinding::setNativeProps
Summary: Pretty straightforward.

Reviewed By: sahrens

Differential Revision: D13114836

fbshipit-source-id: ff86b28d8764955383d4b46a369faf978825f6eb
2018-11-27 18:03:27 -08:00
Valentin Shergin
142dd7673e Fabric: UIManager::setNativeProps
Summary: Pretty straightforward wiring UIManager and the new feature in ShadowTree: we get the node, clone with the new props and then replace this.

Reviewed By: sahrens

Differential Revision: D13114788

fbshipit-source-id: 3a34fb879f3ec564c26278034a19b88518302de8
2018-11-27 18:03:27 -08:00
Valentin Shergin
f42d2b9e32 Fabric: ShadowTree::completeByReplacingShadowNode
Summary: This method is the core of the future features: `setNativeProps` and `LocalState`.

Reviewed By: sahrens

Differential Revision: D13114789

fbshipit-source-id: 2138496c43c171fe27784b1959d86d6eec4638ee
2018-11-27 18:03:27 -08:00
Valentin Shergin
d594d5a4e5 Fabric: UIManagerBinding::getRelativeLayoutMetrics
Summary: Exposing the getRelativeLayoutMetrics method to JS.

Reviewed By: mdvacca

Differential Revision: D13036552

fbshipit-source-id: de825dfde8e64163168510aea1eda77370753b29
2018-11-27 18:03:27 -08:00
Valentin Shergin
e61a14e71d Fabric: RCTWeakEventEmitterWrapper, NSAttributedString and co.
Summary:
Previously, we stored a pointer to ShadowNode inside NSAttributedString's attributes to make possible retrieving an EventEmitter associated with some text fragment.
That worked fine besides only one caveat: the internal implementation of NSAttributedString is quite strange and that causes a memory leak. Because of some reason, NSAttributedString does not release stored attributes after own destruction (maybe OS uses some kind of caching).
So, now, instead of storing a strong pointer to ShadowNode inside NSAttributedString, we store a weak pointer to EventEmitter. Storing a weak pointer is okay because a desired lifetime of EventEmitter is guaranteed by LocalData stored inside a View. Storing a weak EventEmitter instead of weak ShadowNode will also help us with migration to ShadowView (we cannot store ShadowView weakly because it's a stack allocated object).

Reviewed By: sahrens

Differential Revision: D13196886

fbshipit-source-id: f8714e4b3709765629d6456edf0c635bf5f7c53b
2018-11-26 13:51:19 -08:00
Valentin Shergin
0ec1d21c2f Fabric: Fixed a retain cycle between ParagraphShadowNode, LocalData and AttributedString
Summary:
Consider this:
 * ParagraphShadowNode retains LocalData,
 * LocalData contains AttributedString,
 * AttributedString contains Fragments,
 * Fragment can contain a pointer to parent shadow node, so it can be the ParagraphShadowNode.

In this case it's a retain cycle.
We actually don't need to store pointers to not TextShadowNodes, so we don't now.

Later, after we fully migrate to ShadowView, we can remove this condition because it will become harmless.

Reviewed By: sahrens

Differential Revision: D13196885

fbshipit-source-id: d386ce0a067df0a72e6619d62d56038aaf80eccb
2018-11-26 13:51:19 -08:00
Valentin Shergin
cd5f0bd95c Fabric: Stopping creating ShadowView instances for non-View ShadowNodes
Summary:
Apparently, the previous behavior brings more problems than some *possible-in-the-future* features and flexibility.
The new model allows us to easily implement "nested text" feature.

(We temporary hope the old behavious for Android only for compatibility reasons.)

Reviewed By: mdvacca

Differential Revision: D13176277

fbshipit-source-id: 01f7bfb3c2e70cc89d76ecb78add016ee91cbd63
2018-11-25 22:17:30 -08:00