diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index f1c91f3517..136572e12a 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1161,6 +1161,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js * preserves a previously rendered node when deprioritized * can reuse side-effects after being preempted * can reuse side-effects after being preempted, if shouldComponentUpdate is false +* can update a completed tree before it has a chance to commit * updates a child even though the old props is empty * can defer side-effects and resume them later on * can defer side-effects and reuse them later - complex diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 58cfbd19bb..40466f814f 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -143,6 +143,9 @@ module.exports = function(config : HostConfig) : root.pendingContext = getContextForSubtree(parentComponent); // TODO: Use pending work/state instead of props. root.current.pendingProps = element; + if (root.current.alternate) { + root.current.alternate.pendingProps = element; + } scheduleWork(root); @@ -156,6 +159,9 @@ module.exports = function(config : HostConfig) : const root : FiberRoot = (container.stateNode : any); // TODO: Use pending work/state instead of props. root.current.pendingProps = []; + if (root.current.alternate) { + root.current.alternate.pendingProps = []; + } scheduleWork(root); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 2798912747..2b2b32e061 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -89,6 +89,8 @@ module.exports = function(config : HostConfig) { let nextUnitOfWork : ?Fiber = null; let nextPriorityLevel : PriorityLevel = NoWork; + let pendingCommit : Fiber | null = null; + // Linked list of roots with scheduled work on them. let nextScheduledRoot : ?FiberRoot = null; let lastScheduledRoot : ?FiberRoot = null; @@ -115,6 +117,10 @@ module.exports = function(config : HostConfig) { } function findNextUnitOfWork() { + if (pendingCommit) { + return null; + } + // Clear out roots with no more work on them, or if they have uncaught errors while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) { // Unschedule this root. @@ -160,6 +166,17 @@ module.exports = function(config : HostConfig) { } function commitAllWork(finishedWork : Fiber) { + pendingCommit = null; + const root : FiberRoot = (finishedWork.stateNode : any); + resetWorkPriority(finishedWork); + if (root.current === finishedWork) { + throw new Error( + 'Cannot commit the same tree as before. This is probably a bug ' + + 'related to the return field.' + ); + } + root.current = finishedWork; + prepareForCommit(); // Commit all the side-effects within a tree. @@ -293,11 +310,16 @@ module.exports = function(config : HostConfig) { } } - // Flush any task work that was scheduled during this batch - performTaskWork(); + return null; } function resetWorkPriority(workInProgress : Fiber) { + if (workInProgress.pendingProps) { + // Don't reset the priority if the workInProgress has pending props. This + // could happen if a completed root is updated before it is able to commit. + return; + } + let newPriority = NoWork; // progressedChild is going to be the child set with the highest priority. // Either it is the same as child, or it just bailed out because it choose @@ -324,20 +346,30 @@ module.exports = function(config : HostConfig) { const current = workInProgress.alternate; const next = completeWork(current, workInProgress); - resetWorkPriority(workInProgress); - // The work is now done. We don't need this anymore. This flags // to the system not to redo any work here. workInProgress.pendingProps = null; workInProgress.updateQueue = null; + const returnFiber = workInProgress.return; + const siblingFiber = workInProgress.sibling; + const isRoot = !returnFiber && !siblingFiber; + + if (!isRoot) { + resetWorkPriority(workInProgress); + } + if (next) { // If completing this work spawned new work, do that next. We'll come // back here again. return next; } - const returnFiber = workInProgress.return; + if (isRoot) { + // If we've reached the root, commit during the next unit of work. + pendingCommit = workInProgress; + return workInProgress; + } if (returnFiber) { // Append all the effects of the subtree and this fiber onto the effect @@ -369,37 +401,15 @@ module.exports = function(config : HostConfig) { } } - if (workInProgress.sibling) { + if (siblingFiber) { // If there is more work to do in this returnFiber, do that next. - return workInProgress.sibling; + return siblingFiber; } else if (returnFiber) { // If there's no more work in this returnFiber. Complete the returnFiber. workInProgress = returnFiber; continue; } else { - // If we're at the root, there's no more work to do. We can flush it. - const root : FiberRoot = (workInProgress.stateNode : any); - if (root.current === workInProgress) { - throw new Error( - 'Cannot commit the same tree as before. This is probably a bug ' + - 'related to the return field.' - ); - } - root.current = workInProgress; - // TODO: We can be smarter here and only look for more work in the - // "next" scheduled work since we've already scanned passed. That - // also ensures that work scheduled during reconciliation gets deferred. - // const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork; - commitAllWork(workInProgress); - const nextWork = findNextUnitOfWork(); - // if (!nextWork && hasMoreWork) { - // TODO: This can happen when some deep work completes and we don't - // know if this was the last one. We should be able to keep track of - // the highest priority still in the tree for one pass. But if we - // terminate an update we don't know. - // throw new Error('FiberRoots should not have flagged more work if there is none.'); - // } - return nextWork; + throw new Error('Should have already handled root.'); } } } @@ -445,13 +455,17 @@ module.exports = function(config : HostConfig) { } function performDeferredWorkUnsafe(deadline) { - if (!nextUnitOfWork) { + if (!nextUnitOfWork && !pendingCommit) { nextUnitOfWork = findNextUnitOfWork(); } - while (nextUnitOfWork) { + while (nextUnitOfWork || pendingCommit) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - if (!nextUnitOfWork) { + if (pendingCommit) { + nextUnitOfWork = commitAllWork(pendingCommit); + } else if (nextUnitOfWork) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + if (!nextUnitOfWork && !pendingCommit) { // Find more work. We might have time to complete some more. nextUnitOfWork = findNextUnitOfWork(); } @@ -468,12 +482,19 @@ module.exports = function(config : HostConfig) { } function performAnimationWorkUnsafe() { - // Always start from the root - nextUnitOfWork = findNextUnitOfWork(); - while (nextUnitOfWork && - nextPriorityLevel === AnimationPriority) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - if (!nextUnitOfWork) { + if (!nextUnitOfWork && !pendingCommit) { + nextUnitOfWork = findNextUnitOfWork(); + } + while ( + pendingCommit || + (nextUnitOfWork && nextPriorityLevel === AnimationPriority) + ) { + if (pendingCommit) { + nextUnitOfWork = commitAllWork(pendingCommit); + } else if (nextUnitOfWork) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + if (!nextUnitOfWork && !pendingCommit) { // Keep searching for animation work until there's no more left nextUnitOfWork = findNextUnitOfWork(); } @@ -489,12 +510,20 @@ module.exports = function(config : HostConfig) { } function performSynchronousWorkUnsafe() { - nextUnitOfWork = findNextUnitOfWork(); - while (nextUnitOfWork && - nextPriorityLevel === SynchronousPriority) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - - if (!nextUnitOfWork) { + if (!nextUnitOfWork && !pendingCommit) { + nextUnitOfWork = findNextUnitOfWork(); + } + while ( + pendingCommit || + (nextUnitOfWork && nextPriorityLevel === SynchronousPriority) + ) { + if (pendingCommit) { + nextUnitOfWork = commitAllWork(pendingCommit); + } else if (nextUnitOfWork) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + if (!nextUnitOfWork && !pendingCommit) { + // Keep searching for sync work until there's no more left nextUnitOfWork = findNextUnitOfWork(); } } @@ -514,13 +543,20 @@ module.exports = function(config : HostConfig) { isPerformingTaskWork = true; try { - nextUnitOfWork = findNextUnitOfWork(); - while (nextUnitOfWork && - nextPriorityLevel === TaskPriority) { - nextUnitOfWork = - performUnitOfWork(nextUnitOfWork); - - if (!nextUnitOfWork) { + if (!nextUnitOfWork && !pendingCommit) { + nextUnitOfWork = findNextUnitOfWork(); + } + while ( + (pendingCommit && pendingCommit.pendingWorkPriority === TaskPriority) || + (nextUnitOfWork && nextPriorityLevel === TaskPriority) + ) { + if (pendingCommit && pendingCommit.pendingWorkPriority === TaskPriority) { + nextUnitOfWork = commitAllWork(pendingCommit); + } else if (nextUnitOfWork) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + if (!nextUnitOfWork && !pendingCommit) { + // Keep searching for sync work until there's no more left nextUnitOfWork = findNextUnitOfWork(); } } @@ -597,6 +633,11 @@ module.exports = function(config : HostConfig) { } } + // Flush any task work that was scheduled during this batch + if (priorityLevel !== TaskPriority) { + performTaskWork(); + } + // Throw the first uncaught error if (!nextUnitOfWork && firstUncaughtError) { let e = firstUncaughtError; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 9db5158afe..2ae928e98d 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -410,7 +410,7 @@ describe('ReactIncremental', () => { // Make a quick update which will create a low pri tree on top of the // already low pri tree. ReactNoop.render(); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ops).toEqual(['Foo']); @@ -491,7 +491,7 @@ describe('ReactIncremental', () => { // Init ReactNoop.render(); - ReactNoop.flushDeferredPri(55 + 25 + 5); + ReactNoop.flushDeferredPri(55 + 25 + 5 + 5); // We only finish the higher priority work. So the low pri content // has not yet finished mounting. @@ -520,7 +520,7 @@ describe('ReactIncremental', () => { ops = []; // The middle content is now pending rendering... - ReactNoop.flushDeferredPri(30); + ReactNoop.flushDeferredPri(30 + 5); expect(ops).toEqual(['Middle', 'Bar']); ops = []; @@ -600,7 +600,7 @@ describe('ReactIncremental', () => { // Make a quick update which will schedule low priority work to // update the middle content. ReactNoop.render(); - ReactNoop.flushDeferredPri(30); + ReactNoop.flushDeferredPri(30 + 5); expect(ops).toEqual(['Foo', 'Bar']); diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js index 1a379edb0b..ca56d458c5 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js @@ -88,7 +88,7 @@ describe('ReactIncrementalErrorHandling', () => { expect(ReactNoop.getChildren()).toEqual([]); ops.length = 0; - ReactNoop.flushDeferredPri(30); + ReactNoop.flushDeferredPri(30 + 5 + 5 + 5); expect(ops).toEqual([ 'BrokenRender', 'ErrorBoundary unstable_handleError', @@ -134,7 +134,7 @@ describe('ReactIncrementalErrorHandling', () => { 'BrokenRender', ]); ops = []; - ReactNoop.flushDeferredPri(25); + ReactNoop.flushDeferredPri(25 + 5 + 5); expect(ops).toEqual([ 'ErrorBoundary unstable_handleError', 'ErrorBoundary render error', diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js index e272302e2b..7653a02993 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js @@ -144,7 +144,7 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.flushDeferredPri(10); expect(ReactNoop.getChildren()).toEqual([]); - ReactNoop.flushDeferredPri(10); + ReactNoop.flushDeferredPri(10 + 5); expect(ReactNoop.getChildren()).toEqual([span('2')]); }); @@ -159,17 +159,17 @@ describe('ReactIncrementalScheduling', () => { expect(ReactNoop.getChildren('b')).toEqual([]); expect(ReactNoop.getChildren('c')).toEqual([]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([]); expect(ReactNoop.getChildren('c')).toEqual([]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual([]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]); @@ -329,19 +329,19 @@ describe('ReactIncrementalScheduling', () => { it('splits deferred work on multiple roots', () => { // Schedule one root ReactNoop.renderToRootWithID(, 'a'); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); // Schedule two roots ReactNoop.renderToRootWithID(, 'a'); ReactNoop.renderToRootWithID(, 'b'); // First scheduled one gets processed first - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([]); expect(ReactNoop.getChildren('c')).toEqual(null); // Then the second one gets processed - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual(null); @@ -351,15 +351,15 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.renderToRootWithID(, 'b'); ReactNoop.renderToRootWithID(, 'c'); // They get processed in the order they were scheduled - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual([]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]); expect(ReactNoop.getChildren('c')).toEqual([]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:3')]); @@ -368,7 +368,7 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.renderToRootWithID(, 'a'); ReactNoop.renderToRootWithID(, 'a'); ReactNoop.renderToRootWithID(, 'a'); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:6')]); }); @@ -385,18 +385,18 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.renderToRootWithID(, 'c'); ReactNoop.renderToRootWithID(, 'b'); // Ensure it starts in the order it was scheduled - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]); // Schedule last bit of work, it will get processed the last ReactNoop.renderToRootWithID(, 'a'); // Keep performing work in the order it was scheduled - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]); @@ -419,11 +419,11 @@ describe('ReactIncrementalScheduling', () => { }); // We're flushing deferred work // Still, roots with animation work are handled first - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]); @@ -434,17 +434,17 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.renderToRootWithID(, 'b'); }); // Animation is still handled first - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]); // Finally we handle deferred root in the order it was scheduled - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]); - ReactNoop.flushDeferredPri(15); + ReactNoop.flushDeferredPri(15 + 5); expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]); expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:3')]); @@ -483,7 +483,7 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.render(); - ReactNoop.flushDeferredPri(20); + ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual([ 'render: 0', 'componentDidMount (before setState): 0', @@ -496,7 +496,7 @@ describe('ReactIncrementalScheduling', () => { ops = []; instance.setState({ tick: 2 }); - ReactNoop.flushDeferredPri(20); + ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual([ 'render: 2', @@ -545,7 +545,7 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.render(); - ReactNoop.flushDeferredPri(20); + ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual([ 'render: 0', 'componentDidMount (before setState): 0', @@ -566,7 +566,7 @@ describe('ReactIncrementalScheduling', () => { ops = []; instance.setState({ tick: 2 }); - ReactNoop.flushDeferredPri(20); + ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual([ 'render: 2', diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index 9e64dc43c1..10227db489 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -480,6 +480,38 @@ describe('ReactIncrementalSideEffects', () => { ]); }); + it('can update a completed tree before it has a chance to commit', () => { + function Foo(props) { + return ; + } + ReactNoop.render(); + // This should be just enough to complete the tree without committing it + ReactNoop.flushDeferredPri(20); + expect(ReactNoop.getChildren()).toEqual([]); + // To confirm, perform one more unit of work. The tree should now be flushed. + // (ReactNoop decrements the time remaining by 5 *before* returning it from + // the deadline, so to perform n units of work, you need to give it 5n + 5. + // TODO: This is confusing. Decerement it after.) + ReactNoop.flushDeferredPri(10); + expect(ReactNoop.getChildren()).toEqual([span(1)]); + + ReactNoop.render(); + // This should be just enough to complete the tree without committing it + ReactNoop.flushDeferredPri(20); + expect(ReactNoop.getChildren()).toEqual([span(1)]); + // This time, before we commit the tree, we update the root component with + // new props + ReactNoop.render(); + // Now let's commit. We already had a commit that was pending, which will + // render 2. + ReactNoop.flushDeferredPri(10); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + // If we flush the rest of the work, we should get another commit that + // renders 3. If it renders 2 again, that means an update was dropped. + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span(3)]); + }); + it('updates a child even though the old props is empty', () => { function Foo(props) { return ( @@ -611,7 +643,7 @@ describe('ReactIncrementalSideEffects', () => { ); } ReactNoop.render(); - ReactNoop.flushDeferredPri(65); + ReactNoop.flushDeferredPri(65 + 5); expect(ReactNoop.getChildren()).toEqual([ div( span(0), @@ -770,7 +802,7 @@ describe('ReactIncrementalSideEffects', () => { ops = []; ReactNoop.render(); - ReactNoop.flushDeferredPri(70); + ReactNoop.flushDeferredPri(70 + 5); expect(ReactNoop.getChildren()).toEqual([ div( // Updated. @@ -793,7 +825,7 @@ describe('ReactIncrementalSideEffects', () => { // items. Including the set state since that is deprioritized. // TODO: The cycles it takes to do this could be lowered with further // optimizations. - ReactNoop.flushDeferredPri(60); + ReactNoop.flushDeferredPri(60 + 5); expect(ReactNoop.getChildren()).toEqual([ div( // Updated.