Commit Graph

167 Commits

Author SHA1 Message Date
Erik Eckstein
ddd0f4d3a9 SIL: replace SmallPtrSet<SILBasicBlock *> with BasicBlockSet in LinearLifetimeChecker
This makes it unnecessary to share a single instance of visited blocks across multiple calls the the checker.
2021-01-27 10:31:17 +01:00
Meghana Gupta
845e63f901 Support ownershipKind in SILSSAUpdater 2021-01-21 16:27:50 -08:00
Michael Gottesman
c026e95cce [ownership] Extract out SILOwnershipKind from ValueOwnershipKind into its own type and rename Invalid -> Any.
This makes it easier to understand conceptually why a ValueOwnershipKind with
Any ownership is invalid and also allowed me to explicitly document the lattice
that relates ownership constraints/value ownership kinds.
2020-11-10 14:29:11 -08:00
Andrew Trick
5ae231eaab Rename getFieldNo() to getFieldIndex().
Do I really need to justify this?
2020-09-24 22:44:13 -07:00
Michael Gottesman
d064241599 [ssa-updater] Modernize style before adding support for guaranteed parameters.
Specifically:

1. I made methods, variables camelCase.
2. I expanded out variable names (e.x.: bb -> block, predBB -> predBlocks, U -> wrappedUse).
3. I changed typedef -> using.
4. I changed a few c style for loops into for each loops using llvm::enumerate.

NOTE: I left the parts needed for syncing to LLVM in the old style since LLVM
needs these to exist for CRTP to work correctly for the SILSSAUpdater.
2020-08-06 15:41:00 -07:00
Michael Gottesman
78c3be9052 [ownership] Change LLChecker APIs that use an out array to return leaked blocks to caller to instead use a callback.
NFC.
2020-03-24 21:47:40 -07:00
Michael Gottesman
c55856e372 [pred-memopt] Simplify logic around lifetime extending a value that is consumed in a loop.
I realized after 84232cf177 started testing that I
can simplify this code as such, but I decided to finish testing. This is a NFC
change.
2020-03-24 13:41:08 -07:00
Michael Gottesman
84232cf177 [ownership] Replace all uses outside of the SILOwnershipVerifier of LinearLifetimeChecker::checkValue() and make it a private implementation detail.
HOW THIS WAS DONE: I did this by refactoring the last usages of checkValue into
a higher level API that uses checkValue as an implementation detail:
completeConsumingUseSet(...). All of these places in
MandatoryInlining/PredictableMemOpts all wanted behavior where we complete a set
of consuming uses for a value, detecting if the consuming use is in a different
loop nest from the value.

WHY DO THIS: The reason why I wanted to do this is that checkValue is a lower
level API that drives the actual low level computation. We shouldn't expose its
interface to the LinearLifetimeChecker's users since it is our own private
implementation detail that also has some sharp edges.

AN ADDITIONAL BENEFIT: Additionally by hiding the declaration of checkValue, the
last public use of LinearLifetimeError and ErrorBehaviorKind was not
private. This allowed me to then move the declarations of those two to a private
header (./lib/SIL/LinearLifetimeCheckerPrivate.h) and make their declarations
private to LinearLifetimeChecker as well. As such, I renamed them to
LinearLifetimeChecker::Error and LinearLifetimeChecker::ErrorBehaviorKind.
2020-03-23 20:42:53 -07:00
Michael Gottesman
47e74473f1 [ownership] Rename BorrowScopeIntroducingValue -> BorrowedValue and BorrowScopeOperand -> BorrowingOperand.
Andy and I for some time have been discussing the right name for these two
"ownership concepts". What we realized is that the "ing" on
BorrowScopeIntroducingValue is very unfortunate since this value is the result
of a new borrow scope being introduced. So the name should be really:
BorrowScopeIntroducedValue. Given that is sort of unsatisfying, we settled on
the name BorrowedValue.

Once we found the name BorrowedValue, we naturally realized that
BorrowScopeOperand -> BorrowingOperand followed. This is because the operand is
the operand of the instruction that is creating the new borrow scope. So in a
sense the Operand is the "Use" that causes the original value to become
borrowed. So a BorrowingOperand is where the action is and is "active".
2020-03-18 19:43:30 -07:00
Michael Gottesman
be16822af9 [ownership] Remove BranchPropagatedUser.
The only reason why BranchPropagatedUser existed was because early on in SIL, we
weren't sure if cond_br should be able to handle non-trivial values in
ossa. Now, we have reached the point where we have enough experience to make the
judgement that it is not worth having in the representation due to it not
holding its weight.

Now that in ToT we have banned cond_br from having non-trivial operands in ossa,
I can just eliminate BranchPropagatedUser and replace it with the operands that
we used to construct them!

A few notes:

1. Part of my motiviation in doing this is that I want to change LiveRange to
store operands instead of instructions. This is because we are interested in
being able to understand the LiveRange at a use granularity in cases where we
have multiple operands. While doing this, I discovered that I needed
SILInstructions to use the Linear Lifetime Checker. Then I realized that now was
the time to just unwind BranchPropagatedUser.

2. In certain places in SemanticARCOpts, I had to do add some extra copies to
transform arrays of instructions from LiveRange into their operand form. I am
going to remove them in a subsequent commit when I change LiveRange to work on
operands. I am doing this split to be incremental.

3. I changed isSingleInitAllocStack to have an out array of Operand *. The only
user of this code is today in SemanticARCOpts and this information is fed to the
Linear Lifetime Checker, so I needed to do it.
2020-03-04 07:35:23 -08:00
Michael Gottesman
847ad07847 [ownership] Move LinearLifetimeChecker from OwnershipUtils.h -> LinearLifetimeChecker.h
OwnershipUtils.h is growing a bit and I want to use it to store abstract higher
level utilities for working with ossa. LinearLifetimeChecker is just a low level
detail of that, so it makes sense to move it out now.
2020-03-03 16:15:25 -08:00
Michael Gottesman
6e5f036bda [ownership] Create a single introducer version of getUnderlyingBorrowIntroducingValues and rename it to getAllBorrowIntroducingValues(...).
I also added a comment to getAllBorrowIntroducingValues(...) that explained the
situations where one could have multiple borrow introducing values:

1. True phi arguments.
2. Aggregate forming instructions.
2020-03-01 17:30:53 -08:00
Michael Gottesman
b44fbaeb87 [sil] Use FrozenMultiMap in PredictableMemOpts instead of implementing the data structure by hand. 2020-02-17 17:07:33 -08:00
Michael Gottesman
990af4bdd3 [sil] Change TermInst::getSuccessorBlockArguments() to yield SILArguments instead of SILPhiArgument since I am going to be adding SwitchEnumResult (which the API can vend as well). 2020-01-03 11:12:48 -08:00
Michael Gottesman
321185e82f [sil] Rename TermInst::{getSuccessorBlockArguments,getSuccessorBlockArgumentLists}()
This method returns argument lists, not arguments! We should add in the future
an additional API that returns a flap mapped range over all such argument lists
to cleanup some of this code. But at least now the name is more accurate.
2019-12-28 15:33:30 -08:00
Ravi Kandhadai
935686460c [SIL Optimization] Create a new utility InstructionDeleter to delete instructions
and eliminate dead code. This is meant to be a replacement for the utility:
recursivelyDeleteTriviallyDeadInstructions. The new utility performs more aggresive
dead-code elimination for ownership SIL.

This patch also migrates most non-force-delete uses of
recursivelyDeleteTriviallyDeadInstructions to the new utility.
and migrates one force-delete use of recursivelyDeleteTriviallyDeadInstructions
(in IRGenPrepare) to use the new utility.
2019-12-18 13:17:17 -08:00
Michael Gottesman
07c625b4b0 [pmo] Eliminate non-determinism by unmixing some iteration order/sorting bisection code.
Specifically, I was abusing some sorting behavior on some arrays that I really
needed to iterate over == non-determinism. To work around these issues, I made
two changes:

1. Rather than using a bit vector to mark copy_values that were handled as part
of phi handling and thus needing a way to map copy_value -> bit vector index, I
instead just added a separate small ptr set called
copyValueProcessedWithPhiNodes.

2. I refactored/changed how copy cleanups were inserted for phi nodes by
constructing a flat 2d-array that is stable sorted by the index of the incoming
value associated with the cleanups. An incoming value's index is the count of
the copy cleanup when we see it for the first time. Thus when we do the stable
sort we will be visiting in cleanup insertion order and also will be doing
insertion order along the incomingValue axis.
2019-11-17 03:48:00 -08:00
Michael Gottesman
6759d82dad Revert "Revert "[pmo] Fix load [copy] like I fixed load_borrow.""
This reverts commit 2b8e266694.

That reapplies 7623367208.
2019-11-17 03:48:00 -08:00
Brent Royal-Gordon
2b8e266694 Revert "[pmo] Fix load [copy] like I fixed load_borrow." 2019-11-16 15:54:12 -08:00
Michael Gottesman
95de4d2929 [pmo] Fix load [copy] like I fixed load_borrow.
This involved fixing a few issues exposed by trying to use the load_borrow code
with load [copy] that were caught in our tests by the ownership
verifier. Specifically:

1. In getUnderlyingBorrowIntroducingValues we were first checking if a user was
a guaranteed forwarding instruction and then doing a check if our value was a
value with None ownership that we want to skip. This caused weird behavior where
one could get the wrong borrow introducers if that trivial value came from a
different guaranteed value.

2. I found via a test case in predictable_memaccess_opts that we needed to
insert compensating destroys for phi nodes after we process all of the incoming
values. The reason why this is needed is that multiple phi nodes could use the
same incoming value. In such a case, we need to treat all of the copy_values
inserted for each phi node as users of that incoming value to prevent us from
inserting too many destroy_value. Consider:

```
bb0:
  %0 = copy_value
  cond_br ..., bb1, bb2

bb1:
  br bb3(%0)

bb2:
  br bb4(%0)
```

If we processed the phi args in bb3, bb4 separately, we would insert an extra 2
destroys in bb1, bb2.

The implementation works by processing each phi and for each incoming value of
the phi appending the value, copy we made for the value to an array. We then
stable sort the array only by value. This then allows us to process ranges of
copies with the same underlying incoming value in a 2nd loop taking advantage of
all of the copies for an incoming value being contiguous in said array. We still
lifetime extend to the load/insert destroy_value for the actual phi (not its
incoming values) in that first loop.

3. I tightened up the invariant in the AvailableValueAggregator that it always
returns values that are newly copied unless we are performing a take. This
involved changing the code in handlePrimitiveValues to always insert copies when
ownership is enabled.

4. I tightened up the identification of intermediate phi nodes by instead of
just checking if the phi node has a single terminator user to instead it having
a single terminator user that has a successor with an inserted phi node that we
know about.
2019-11-15 15:16:58 -08:00
Michael Gottesman
b30efa7566 [pmo] Refactor addHandOffCopyDestroysForPhis to be able to take a load or load_borrow.
Just some small changes to types. NFC.
2019-11-14 16:46:11 -08:00
Arnold Schwaighofer
a30f80457d Merge pull request #28044 from aschwaighofer/sil_type_expansion_context
SIL: Use a TypeExpansionContext in SIL lowering to expand opaque type archetypes as part of lowering
2019-11-12 12:02:20 -08:00
Michael Gottesman
eab9be8f2f [pmo] Fix load_borrow for strong control equivalence issues.
This entailed untangling a few issues. I have purposefully only fixed this for
now for load_borrow to make the change in tree smaller (the reason for my
splitting the code paths in a previous group of commits). I am going to follow
up with a load copy version of the patch. I think that the code should be the
same. The interesting part about this is that all of these code conditions are
caught by the ownership verifier, suggesting to me that the code in the
stdlib/overlays is simple enough that we didn't hit these code patterns.

Anyways, the main change here is that we now eliminate control equivalence
issues arising from @owned values that are not control equivalent being
forwarded into aggregates and phis. This would cause our outer value to be
destroyed early since we would be destroying it once for every time iteration in
a loop.

The way that this is done is that (noting that this is only for borrows today):

* The AvailableValueAggregator always copies values at available value points to
  lifetime extend the values to the load block. In the load block, the
  aggregator will take the incoming values and borrow the value to form tuples,
  structs as needed. This new guaranteed aggregate is then copied. If we do not
  need to form an aggregate, we just copy. Since the copy is in our load block,
  we know that we can use it for our load_borrow. Importantly note how we no
  longer allow for these aggregates to forward owned ownership preventing the
  control equivalence problem above.

* Since the aggregator may use the SSA updater if it finds multiple available
  values, we need to also make sure that any phis are strongly control
  equivalent on all of its incoming values. So we insert copies along those
  edges and then lifetime extend the phi as appropriate.

* Since in all of the above all copy_value inserted by the available value
  aggregator are never consumed, we can just add destroy_values in the
  appropriate places and everything works.
2019-11-11 23:47:36 -08:00
Arnold Schwaighofer
8aaa7b4dc1 SILOptimizer: Pipe through TypeExpansionContext 2019-11-11 14:21:52 -08:00
Michael Gottesman
64eeed835a [pmo] Add an assert that when using SILSSAUpdater in ossa, we only have singular values if the underlying type is either trivial or we have a take.
The reason why this is true is that we will be inserting new copy_values for
each available value implying that we can never have such a singular value.

I also added two test cases that show that we have a singular value with the
trivial type and that works.
2019-11-11 13:18:03 -08:00
Michael Gottesman
55b6ff1f5e [pmo] Expand an assert that validates that we never handlePrimitiveValues when promoting takes.
The key thing to note here is that when we are processing a take, we validate
that at all destroy points we have a fully available value. So we should never
hit this code path which is correct. This assert just makes the assumption
clearer in the small when reading code locally in handlePrimitiveValue.
2019-11-11 13:18:03 -08:00
Michael Gottesman
76e243ce55 [pmo] Allow for the AvailableValueAggregator to know if it is being used to Borrow, Copy, Take.
Previously, we only specified via a boolean value whether or not we were a take
or not. Now we have an enum which is more specific and descriptive. It will also
let me fix an issue that occurs with Borrow/Copy incrementally as separate
patches.
2019-11-11 11:31:38 -08:00
Michael Gottesman
476f401b4a [pmo] NFC. Split addMissingDestroysForCopiedValues into a version for load_borrow and one for load.
This is a pure refactor so I can make some subsequent transformations easier to
do.

I also did a little bit of debridement when there were opportunities to
flatten control flow by eliminating direct checks for one or the other classes.
2019-11-10 15:51:40 -08:00
Michael Gottesman
8d9455e757 [pmo] Rather than bailing from addMissingDestroysForCopiedValues if we are in non-ossa, just always assume we are in ossa in the code and do not call it if we are in non-ossa. 2019-11-04 11:24:33 -08:00
Michael Gottesman
5ecacb9820 [pmo] Cleanup some old code to use the same style. 2019-11-04 11:24:33 -08:00
Michael Gottesman
fad176e423 [pred-memopt] Extract out the computation of available values into a helper.
NFC.
2019-11-03 17:16:43 -08:00
Michael Gottesman
933af097cb [pred-memopt] Split promoteLoadCopy into 3 methods, one for copy_addr, load, load_borrow.
NFC.

This just simplifies the code and makes it easier to reason about. I debried a
little bit as well, but nothing too much.
2019-11-03 17:16:42 -08:00
Andrew Trick
bddc69c8a6 Organize SILOptimizer/Utils headers. Remove Local.h.
The XXOptUtils.h convention is already established and parallels
the SIL/XXUtils convention.

New:
- InstOptUtils.h
- CFGOptUtils.h
- BasicBlockOptUtils.h
- ValueLifetime.h

Removed:
- Local.h
- Two conflicting CFG.h files

This reorganization is helpful before I introduce more
utilities for block cloning similar to SinkAddressProjections.

Move the control flow utilies out of Local.h, which was an
unreadable, unprincipled mess. Rename it to InstOptUtils.h, and
confine it to small APIs for working with individual instructions.
These are the optimizer's additions to /SIL/InstUtils.h.

Rename CFG.h to CFGOptUtils.h and remove the one in /Analysis. Now
there is only SIL/CFG.h, resolving the naming conflict within the
swift project (this has always been a problem for source tools). Limit
this header to low-level APIs for working with branches and CFG edges.

Add BasicBlockOptUtils.h for block level transforms (it makes me sad
that I can't use BBOptUtils.h, but SIL already has
BasicBlockUtils.h). These are larger APIs for cloning or removing
whole blocks.
2019-10-02 11:34:54 -07:00
Michael Gottesman
12aa95ac1e [ownership] Only allow BranchPropagatedUser to be constructed from Operands.
This commit only changes how BranchPropagatedUser is constructed and does not
change the internal representation. This is a result of my noticing that
BranchPropagatedUser could also use an operand internally to represent its
state. To simplify how I am making the change, I am splitting the change into
two PRs that should be easy to validate:

1. A commit that maps how the various users of BranchPropagatedUser have been
constructing BPUs to a single routine that takes an Operand. This leaves
BranchPropagatedUser's internal state alone as well as its user the Linear
Lifetime Checker.

2. A second commit that changes the internal bits of the BranchPropagatedUser to
store an Operand instead of a PointerUnion.

This will allow me to use the first commit to validate the second.
2019-09-23 17:24:55 -07:00
Michael Gottesman
dceca2bc3b [ownership] Create a context structure for the linear lifetime checker.
This is just a simple refactoring commit in preparation for hiding more of the
details of the linear lifetime checker. This is NFC, just moving around code.
2019-09-16 20:13:56 -07:00
Slava Pestov
d434188157 SIL: Refactor TypeConverter to not require a SILModule
This mostly requires changing various entry points to pass around a
TypeConverter instead of a SILModule. I've left behind entry points
that take a SILModule for a few methods like SILType::subst() to
avoid creating even more churn.
2019-09-06 21:50:15 -04:00
Michael Gottesman
ced5f4a752 [pmo] Teach PMO that in ossa, load [take] invalidates an available value.
Example would be a case where we dynamically control if the load [take] occurs
and the load [take] happens with conditional control flow.

This was exposed by the throwing_initializer and failable_initializer
interpreter tests in the test of PolarBear(n:before:during).
2019-07-24 17:02:26 -07:00
Michael Gottesman
ada2f9ce46 [pred-deadalloc-elim] Teach the pass how to eliminate dead allocations that are load [take], store [init] back into memory.
I am doing this to eliminate some differences in codegen before/after
serialization ownership. It just means less of the tests need to be touched when
I flip the switch.

Specifically, today this change allows us to handle certain cases where there is
a dead allocation being used to pass around a value at +1 by performing a load
[take] and then storing a value back into the memory. The general format is an
allocation that only has stores, load [take], and destroy_addr users. Consider
the following SIL:

```
store %x to [init] %mem            (0)
%xhat = load [take] %mem           (1)
%xhat_cast = apply %f(%xhat)       (2)
store %xhat_cast to [init] %mem    (3)
destroy_addr %mem
```

Notice how assuming that we can get rid of the store, we can perform the
following store -> load forwarding:

```
%xhat_cast = apply %f(%x)          (2)
store %xhat_cast to [init] %mem    (3)
destroy_addr %mem
```

In contrast, notice how we get an ownership violation (double consume of %x by
(0) and (2)) if we can not get rid of the store:

```
store %x to [init] %mem
%xhat_cast = apply %f(%x)
store %xhat_cast to [init] %mem    (2)
destroy_addr %mem
```

This is in fact the same condition for promoting a destroy_addr since when a
destroy_addr is a load [take] + destroy_value. So I was able to generalize the
code for destroy_addr to handle this case.
2019-05-31 12:12:54 -07:00
Slava Pestov
16d5716e71 SIL: Use the best resilience expansion when lowering types
This is a large patch; I couldn't split it up further while still
keeping things working. There are four things being changed at
once here:

- Places that call SILType::isAddressOnly()/isLoadable() now call
  the SILFunction overload and not the SILModule one.

- SILFunction's overloads of getTypeLowering() and getLoweredType()
  now pass the function's resilience expansion down, instead of
  hardcoding ResilienceExpansion::Minimal.

- Various other places with '// FIXME: Expansion' now use a better
  resilience expansion.

- A few tests were updated to reflect SILGen's improved code
  generation, and some new tests are added to cover more code paths
  that previously were uncovered and only manifested themselves as
  standard library build failures while I was working on this change.
2019-04-26 22:47:59 -04:00
Slava Pestov
8915f96e3e SIL: Replace SILType::isTrivial(SILModule) with isTrivial(SILFunction) 2019-03-12 01:16:04 -04:00
Slava Pestov
c791c4a137 SIL: SILUndef must be aware of the resilience expansion
The ownership kind is Any for trivial types, or Owned otherwise, but
whether a type is trivial or not will soon depend on the resilience
expansion.

This means that a SILModule now uniques two SILUndefs per type instead
of one, and serialization uses two distinct sentinel IDs for this
purpose as well.

For now, the resilience expansion is not actually used here, so this
change is NFC, other than changing the module format.
2019-03-12 00:30:35 -04:00
Slava Pestov
5847e163c1 SIL: Use better type lowering APIs in a couple of spots 2019-03-05 20:59:58 -05:00
Michael Gottesman
a310f23b8a [ownership] Add support for load_borrow in predictable mem opt.
This reduces the diff in between -Onone output when stripping before/after
serialization.

We support load_borrow by translating it to the load [copy] case. Specifically,
for +1, we normally perform the following transform.

  store %1 to [init] %0
  ...
  %2 = load [copy] %0
  ...
  use(%2)
  ...
  destroy_value %2

=>

  %1a = copy_value %1
  store %1 to [init] %0
  ...
  use(%1a)
  ...
  destroy_value %1a

We analogously can optimize load_borrow by replacing the load with a
begin_borrow:

  store %1 to [init] %0
  ...
  %2 = load_borrow %0
  ...
  use(%2)
  ...
  end_borrow %2

=>

  %1a = copy_value %1
  store %1 to [init] %0
  ...
  %2 = begin_borrow %1a
  ...
  use(%2)
  ...
  end_borrow %2
  destroy_value %1a

The store from outside a loop being used by a load_borrow inside a loop is a
similar transformation as the +0 version except that we use a begin_borrow
inside the loop instead of a copy_value (making it even more efficient).
2019-02-11 00:54:28 -08:00
Michael Gottesman
ac6f318c4d [ownership] Fix the linear lifetime checker to emit leak fixups for certain loops it didn't handle correctly before.
Specifically:

bb0:
  br bb1

bb1:
  cond_br ..., bb2, bb3

bb2:
  br bb1

bb3:
  return

What would happen in this case is that we wouldn't revisit bb1 after we found
the double consume to grab the leak (and would early exit as well). So we would
not insert a destroy for the out of loop value causing a leak from the
perspective of the ownership checker. This was due to us early exiting and also
due to us not revisiting bb1 after we went around the backedge from bb2 ->
bb1. Now what we do instead is if we catch a double consume in a block we have
already visited, we check if we haven't visited any of that block's
successors. If we haven't, we add that to the list of successors we need to
visit.
2019-01-29 16:39:58 -08:00
Michael Gottesman
224c8a5801 [ownership] Use a richer error result than bool.
I am starting to use the linear lifetime checker in an optimizer role where it
no longer asserts but instead tells the optimizer pass what is needed to cause
the lifetime to be linear. To do so I need to be able to return richer
information to the caller such as whether or not a leak, double consume, or
use-after-free occurs.
2019-01-28 12:47:40 -08:00
Michael Gottesman
ddcce66980 [pmo] Translate pred mem opt test for ownership and eliminate some unneeded Phis getting inserted by the SSA updater.
We already do the SSA updater optimization if we have an available value in the
same block. If we do not have such an available value, we insert the Phis
despite all of the available values being the same. The small change here just
fixes that issue.
2019-01-23 14:27:23 -08:00
Michael Gottesman
93282772e7 [pmo] Update predictable dead alloc elimination for ownership.
I also translated predictable_deadalloc_elim.sil into an ownership test and
added more tests that double checks the ownership specific functionality. This
change should be NFC without ownership.
2019-01-22 11:45:36 -08:00
swift-ci
5b3f128479 Merge pull request #22041 from gottesmm/pr-6a1df2251889fc8954e6057a15e57e4ec130a20c 2019-01-22 10:55:58 -08:00
Michael Gottesman
14a9dcbe9a [pmo] Refactor into helper isFullyAvailable the fully available check from aggregateFullyAvailableValue.
This is NFC. I am going to use this check in another part of the code to verify
that we can split up destroy_addr without needing to destructure available
values.
2019-01-22 09:50:01 -08:00
Michael Gottesman
04b60bb1f6 [pmo] Use the linear lifetime checker to insert compensating destroys given only non-postdominating uses.
With ownership PMO needs to insert copies before the stores that provide an
available value. Usually, we are only using the available value along a single
path. This change ensures that if in that situation, we have "leaking paths"
besides that single path, PMO inserts compensating destroys.

NOTE: Without ownership enabled this is NFC.
2019-01-22 09:36:15 -08:00