• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.tuner.exoplayer.buffer;
18 
19 import android.media.MediaFormat;
20 import android.os.ConditionVariable;
21 import android.support.annotation.NonNull;
22 import android.support.annotation.VisibleForTesting;
23 import android.util.ArrayMap;
24 import android.util.Log;
25 import android.util.Pair;
26 
27 import com.google.android.exoplayer.SampleHolder;
28 import com.android.tv.tuner.exoplayer.SampleExtractor;
29 import com.android.tv.util.Utils;
30 
31 import java.io.File;
32 import java.io.FileNotFoundException;
33 import java.io.IOException;
34 import java.util.ArrayList;
35 import java.util.LinkedList;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.SortedMap;
40 import java.util.TreeMap;
41 
42 /**
43  * Manages {@link SampleChunk} objects.
44  * <p>
45  * The buffer manager can be disabled, while running, if the write throughput to the associated
46  * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}".
47  * This leads to restarting playback flow.
48  */
49 public class BufferManager {
50     private static final String TAG = "BufferManager";
51     private static final boolean DEBUG = false;
52 
53     // Constants for the disk write speed checking
54     private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK =
55             10L * 1024 * 1024;  // Checks for every 10M disk write
56     private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024;
57     private static final int MAXIMUM_SPEED_CHECK_COUNT = 5;  // Checks only 5 times
58     private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3;  // 3 Megabytes per second
59 
60     private final SampleChunk.SampleChunkCreator mSampleChunkCreator;
61     // Maps from track name to a map which maps from starting position to {@link SampleChunk}.
62     private final Map<String, SortedMap<Long, SampleChunk>> mChunkMap = new ArrayMap<>();
63     private final Map<String, Long> mStartPositionMap = new ArrayMap<>();
64     private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>();
65     private final StorageManager mStorageManager;
66     private long mBufferSize = 0;
67     private final EvictChunkQueueMap mPendingDelete = new EvictChunkQueueMap();
68     private final SampleChunk.ChunkCallback mChunkCallback = new SampleChunk.ChunkCallback() {
69         @Override
70         public void onChunkWrite(SampleChunk chunk) {
71             mBufferSize += chunk.getSize();
72         }
73 
74         @Override
75         public void onChunkDelete(SampleChunk chunk) {
76             mBufferSize -= chunk.getSize();
77         }
78     };
79 
80     private volatile boolean mClosed = false;
81     private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
82     private long mTotalWriteSize;
83     private long mTotalWriteTimeNs;
84     private float mWriteBandwidth = 0.0f;
85     private volatile int mSpeedCheckCount;
86     private boolean mDisabled = false;
87 
88     public interface ChunkEvictedListener {
onChunkEvicted(String id, long createdTimeMs)89         void onChunkEvicted(String id, long createdTimeMs);
90     }
91     /**
92      * Handles I/O
93      * between BufferManager and {@link SampleExtractor}.
94      */
95     public interface SampleBuffer {
96 
97         /**
98          * Initializes SampleBuffer.
99          * @param Ids track identifiers for storage read/write.
100          * @param mediaFormats meta-data for each track.
101          * @throws IOException
102          */
init(@onNull List<String> Ids, @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats)103         void init(@NonNull List<String> Ids,
104                   @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats)
105                 throws IOException;
106 
107         /**
108          * Selects the track {@code index} for reading sample data.
109          */
selectTrack(int index)110         void selectTrack(int index);
111 
112         /**
113          * Deselects the track at {@code index},
114          * so that no more samples will be read from the track.
115          */
deselectTrack(int index)116         void deselectTrack(int index);
117 
118         /**
119          * Writes sample to storage.
120          *
121          * @param index track index
122          * @param sample sample to write at storage
123          * @param conditionVariable notifies the completion of writing sample.
124          * @throws IOException
125          */
writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)126         void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
127                 throws IOException;
128 
129         /**
130          * Checks whether storage write speed is slow.
131          */
isWriteSpeedSlow(int sampleSize, long writeDurationNs)132         boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs);
133 
134         /**
135          * Handles when write speed is slow.
136          * @throws IOException
137          */
handleWriteSpeedSlow()138         void handleWriteSpeedSlow() throws IOException;
139 
140         /**
141          * Sets the flag when EoS was reached.
142          */
setEos()143         void setEos();
144 
145         /**
146          * Reads the next sample in the track at index {@code track} into {@code sampleHolder},
147          * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ}
148          * if it is available.
149          * If the next sample is not available,
150          * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}.
151          */
readSample(int index, SampleHolder outSample)152         int readSample(int index, SampleHolder outSample);
153 
154         /**
155          * Seeks to the specified time in microseconds.
156          */
seekTo(long positionUs)157         void seekTo(long positionUs);
158 
159         /**
160          * Returns an estimate of the position up to which data is buffered.
161          */
getBufferedPositionUs()162         long getBufferedPositionUs();
163 
164         /**
165          * Returns whether there is buffered data.
166          */
continueBuffering(long positionUs)167         boolean continueBuffering(long positionUs);
168 
169         /**
170          * Cleans up and releases everything.
171          * @throws IOException
172          */
release()173         void release() throws IOException;
174     }
175 
176     /**
177      * Storage configuration and policy manager for {@link BufferManager}
178      */
179     public interface StorageManager {
180 
181         /**
182          * Provides eligible storage directory for {@link BufferManager}.
183          *
184          * @return a directory to save buffer(chunks) and meta files
185          */
getBufferDir()186         File getBufferDir();
187 
188         /**
189          * Cleans up storage.
190          */
clearStorage()191         void clearStorage();
192 
193         /**
194          * Informs whether the storage is used for persistent use. (eg. dvr recording/play)
195          *
196          * @return {@code true} if stored files are persistent
197          */
isPersistent()198         boolean isPersistent();
199 
200         /**
201          * Informs whether the storage usage exceeds pre-determined size.
202          *
203          * @param bufferSize the current total usage of Storage in bytes.
204          * @param pendingDelete the current storage usage which will be deleted in near future by
205          *                      bytes
206          * @return {@code true} if it reached pre-determined max size
207          */
reachedStorageMax(long bufferSize, long pendingDelete)208         boolean reachedStorageMax(long bufferSize, long pendingDelete);
209 
210         /**
211          * Informs whether the storage has enough remained space.
212          *
213          * @param pendingDelete the current storage usage which will be deleted in near future by
214          *                      bytes
215          * @return {@code true} if it has enough space
216          */
hasEnoughBuffer(long pendingDelete)217         boolean hasEnoughBuffer(long pendingDelete);
218 
219         /**
220          * Reads track name & {@link MediaFormat} from storage.
221          *
222          * @param isAudio {@code true} if it is for audio track
223          * @return {@link Pair} of track name & {@link MediaFormat}
224          * @throws IOException
225          */
readTrackInfoFile(boolean isAudio)226         Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException;
227 
228         /**
229          * Reads sample indexes for each written sample from storage.
230          *
231          * @param trackId track name
232          * @return indexes of the specified track
233          * @throws IOException
234          */
readIndexFile(String trackId)235         ArrayList<Long> readIndexFile(String trackId) throws IOException;
236 
237         /**
238          * Writes track information to storage.
239          *
240          * @param trackId track name
241          * @param format {@link android.media.MediaFormat} of the track
242          * @param isAudio {@code true} if it is for audio track
243          * @throws IOException
244          */
writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)245         void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
246                 throws IOException;
247 
248         /**
249          * Writes index file to storage.
250          *
251          * @param trackName track name
252          * @param index {@link SampleChunk} container
253          * @throws IOException
254          */
writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)255         void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
256                 throws IOException;
257     }
258 
259     private static class EvictChunkQueueMap {
260         private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>();
261         private long mSize;
262 
init(String key)263         private void init(String key) {
264             mEvictMap.put(key, new LinkedList<>());
265         }
266 
add(String key, SampleChunk chunk)267         private void add(String key, SampleChunk chunk) {
268             LinkedList<SampleChunk> queue = mEvictMap.get(key);
269             if (queue != null) {
270                 mSize += chunk.getSize();
271                 queue.add(chunk);
272             }
273         }
274 
poll(String key, long startPositionUs)275         private SampleChunk poll(String key, long startPositionUs) {
276             LinkedList<SampleChunk> queue = mEvictMap.get(key);
277             if (queue != null) {
278                 SampleChunk chunk = queue.peek();
279                 if (chunk != null && chunk.getStartPositionUs() < startPositionUs) {
280                     mSize -= chunk.getSize();
281                     return queue.poll();
282                 }
283             }
284             return null;
285         }
286 
getSize()287         private long getSize() {
288             return mSize;
289         }
290 
release()291         private void release() {
292             for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) {
293                 for (SampleChunk chunk : entry.getValue()) {
294                     SampleChunk.IoState.release(chunk, true);
295                 }
296             }
297             mEvictMap.clear();
298             mSize = 0;
299         }
300     }
301 
BufferManager(StorageManager storageManager)302     public BufferManager(StorageManager storageManager) {
303         this(storageManager, new SampleChunk.SampleChunkCreator());
304     }
305 
BufferManager(StorageManager storageManager, SampleChunk.SampleChunkCreator sampleChunkCreator)306     public BufferManager(StorageManager storageManager,
307             SampleChunk.SampleChunkCreator sampleChunkCreator) {
308         mStorageManager = storageManager;
309         mSampleChunkCreator = sampleChunkCreator;
310         clearBuffer(true);
311     }
312 
registerChunkEvictedListener(String id, ChunkEvictedListener listener)313     public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
314         mEvictListeners.put(id, listener);
315     }
316 
unregisterChunkEvictedListener(String id)317     public void unregisterChunkEvictedListener(String id) {
318         mEvictListeners.remove(id);
319     }
320 
clearBuffer(boolean deleteFiles)321     private void clearBuffer(boolean deleteFiles) {
322         mChunkMap.clear();
323         if (deleteFiles) {
324             mStorageManager.clearStorage();
325         }
326         mBufferSize = 0;
327     }
328 
getFileName(String id, long positionUs)329     private static String getFileName(String id, long positionUs) {
330         return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs);
331     }
332 
333     /**
334      * Creates a new {@link SampleChunk} for caching samples.
335      *
336      * @param id the name of the track
337      * @param positionUs starting position of the {@link SampleChunk} in micro seconds.
338      * @param samplePool {@link SamplePool} for the fast creation of samples.
339      * @return returns the created {@link SampleChunk}.
340      * @throws IOException
341      */
createNewWriteFile(String id, long positionUs, SamplePool samplePool)342     public SampleChunk createNewWriteFile(String id, long positionUs,
343             SamplePool samplePool) throws IOException {
344         if (!maybeEvictChunk()) {
345             throw new IOException("Not enough storage space");
346         }
347         SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
348         if (map == null) {
349             map = new TreeMap<>();
350             mChunkMap.put(id, map);
351             mStartPositionMap.put(id, positionUs);
352             mPendingDelete.init(id);
353         }
354         File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
355         SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file,
356                 positionUs, mChunkCallback);
357         map.put(positionUs, sampleChunk);
358         return sampleChunk;
359     }
360 
361     /**
362      * Loads a track using {@link BufferManager.StorageManager}.
363      *
364      * @param trackId the name of the track.
365      * @param samplePool {@link SamplePool} for the fast creation of samples.
366      * @throws IOException
367      */
loadTrackFromStorage(String trackId, SamplePool samplePool)368     public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
369         ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId);
370         long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0;
371 
372         SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId);
373         if (map == null) {
374             map = new TreeMap<>();
375             mChunkMap.put(trackId, map);
376             mStartPositionMap.put(trackId, startPositionUs);
377             mPendingDelete.init(trackId);
378         }
379         SampleChunk chunk = null;
380         for (long positionUs: keyPositions) {
381             chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
382                     mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs,
383                     mChunkCallback, chunk);
384             map.put(positionUs, chunk);
385         }
386     }
387 
388     /**
389      * Finds a {@link SampleChunk} for the specified track name and the position.
390      *
391      * @param id the name of the track.
392      * @param positionUs the position.
393      * @return returns the found {@link SampleChunk}.
394      */
getReadFile(String id, long positionUs)395     public SampleChunk getReadFile(String id, long positionUs) {
396         SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
397         if (map == null) {
398             return null;
399         }
400         SampleChunk sampleChunk;
401         SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1);
402         if (!headMap.isEmpty()) {
403             sampleChunk = headMap.get(headMap.lastKey());
404         } else {
405             sampleChunk = map.get(map.firstKey());
406         }
407         return sampleChunk;
408     }
409 
410     /**
411      * Evicts chunks which are ready to be evicted for the specified track
412      *
413      * @param id the specified track
414      * @param earlierThanPositionUs the start position of the {@link SampleChunk}
415      *                   should be earlier than
416      */
evictChunks(String id, long earlierThanPositionUs)417     public void evictChunks(String id, long earlierThanPositionUs) {
418         SampleChunk chunk = null;
419         while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) {
420             SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent())  ;
421         }
422     }
423 
424     /**
425      * Returns the start position of the specified track in micro seconds.
426      *
427      * @param id the specified track
428      */
getStartPositionUs(String id)429     public long getStartPositionUs(String id) {
430         Long ret = mStartPositionMap.get(id);
431         return ret == null ? 0 : ret;
432     }
433 
maybeEvictChunk()434     private boolean maybeEvictChunk() {
435         long pendingDelete = mPendingDelete.getSize();
436         while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete)
437                 || !mStorageManager.hasEnoughBuffer(pendingDelete)) {
438             if (mStorageManager.isPersistent()) {
439                 // Since chunks are persistent, we cannot evict chunks.
440                 return false;
441             }
442             SortedMap<Long, SampleChunk> earliestChunkMap = null;
443             SampleChunk earliestChunk = null;
444             String earliestChunkId = null;
445             for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
446                 SortedMap<Long, SampleChunk> map = entry.getValue();
447                 if (map.isEmpty()) {
448                     continue;
449                 }
450                 SampleChunk chunk = map.get(map.firstKey());
451                 if (earliestChunk == null
452                         || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
453                     earliestChunkMap = map;
454                     earliestChunk = chunk;
455                     earliestChunkId = entry.getKey();
456                 }
457             }
458             if (earliestChunk == null) {
459                 break;
460             }
461             mPendingDelete.add(earliestChunkId, earliestChunk);
462             earliestChunkMap.remove(earliestChunk.getStartPositionUs());
463             if (DEBUG) {
464                 Log.d(TAG, String.format("bufferSize = %d; pendingDelete = %b; "
465                                 + "earliestChunk size = %d; %s@%d (%s)",
466                         mBufferSize, pendingDelete, earliestChunk.getSize(), earliestChunkId,
467                         earliestChunk.getStartPositionUs(),
468                         Utils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs())));
469             }
470             ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId);
471             if (listener != null) {
472                 listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs());
473             }
474             pendingDelete = mPendingDelete.getSize();
475         }
476         for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
477             SortedMap<Long, SampleChunk> map = entry.getValue();
478             if (map.isEmpty()) {
479                 continue;
480             }
481             mStartPositionMap.put(entry.getKey(), map.firstKey());
482         }
483         return true;
484     }
485 
486     /**
487      * Reads track information which includes {@link MediaFormat}.
488      *
489      * @return returns all track information which is found by {@link BufferManager.StorageManager}.
490      * @throws IOException
491      */
readTrackInfoFiles()492     public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException {
493         ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>();
494         try {
495             trackInfos.add(mStorageManager.readTrackInfoFile(false));
496         } catch (FileNotFoundException e) {
497             // There can be a single track only recording. (eg. audio-only, video-only)
498             // So the exception should not stop the read.
499         }
500         try {
501             trackInfos.add(mStorageManager.readTrackInfoFile(true));
502         } catch (FileNotFoundException e) {
503             // See above catch block.
504         }
505         return trackInfos;
506     }
507 
508     /**
509      * Writes track information and index information for all tracks.
510      *
511      * @param audio audio information.
512      * @param video video information.
513      * @throws IOException
514      */
writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video)515     public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video)
516             throws IOException {
517         if (audio != null) {
518             mStorageManager.writeTrackInfoFile(audio.first, audio.second, true);
519             SortedMap<Long, SampleChunk> map = mChunkMap.get(audio.first);
520             if (map == null) {
521                 throw new IOException("Audio track index missing");
522             }
523             mStorageManager.writeIndexFile(audio.first, map);
524         }
525         if (video != null) {
526             mStorageManager.writeTrackInfoFile(video.first, video.second, false);
527             SortedMap<Long, SampleChunk> map = mChunkMap.get(video.first);
528             if (map == null) {
529                 throw new IOException("Video track index missing");
530             }
531             mStorageManager.writeIndexFile(video.first, map);
532         }
533     }
534 
535     /**
536      * Marks it is closed and it is not used anymore.
537      */
close()538     public void close() {
539         // Clean-up may happen after this is called.
540         mClosed = true;
541     }
542 
543     /**
544      * Releases all the resources.
545      */
release()546     public void release() {
547         mPendingDelete.release();
548         for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
549             for (SampleChunk chunk : entry.getValue().values()) {
550                 SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent());
551             }
552         }
553         mChunkMap.clear();
554         if (mClosed) {
555             clearBuffer(!mStorageManager.isPersistent());
556         }
557     }
558 
resetWriteStat(float writeBandwidth)559     private void resetWriteStat(float writeBandwidth) {
560         mWriteBandwidth = writeBandwidth;
561         mTotalWriteSize = 0;
562         mTotalWriteTimeNs = 0;
563     }
564 
565     /**
566      * Adds a disk write sample size to calculate the average disk write bandwidth.
567      */
addWriteStat(long size, long timeNs)568     public void addWriteStat(long size, long timeNs) {
569         if (size >= mMinSampleSizeForSpeedCheck) {
570             mTotalWriteSize += size;
571             mTotalWriteTimeNs += timeNs;
572         }
573     }
574 
575     /**
576      * Returns if the average disk write bandwidth is slower than
577      * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}.
578      */
isWriteSlow()579     public boolean isWriteSlow() {
580         if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) {
581             return false;
582         }
583 
584         // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers
585         // by temporary system overloading during the playback.
586         if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) {
587             return false;
588         }
589         mSpeedCheckCount++;
590         float megabytePerSecond = calculateWriteBandwidth();
591         resetWriteStat(megabytePerSecond);
592         if (DEBUG) {
593             Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps");
594         }
595         return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS;
596     }
597 
598     /**
599      * Returns recent write bandwidth in MBps. If recent bandwidth is not available,
600      * returns {float -1.0f}.
601      */
getWriteBandwidth()602     public float getWriteBandwidth() {
603         return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth;
604     }
605 
calculateWriteBandwidth()606     private float calculateWriteBandwidth() {
607         if (mTotalWriteTimeNs == 0) {
608             return -1;
609         }
610         return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs);
611     }
612 
613     /**
614      * Marks {@link BufferManager} object disabled to prevent it from the future use.
615      */
disable()616     public void disable() {
617         mDisabled = true;
618     }
619 
620     /**
621      * Returns if {@link BufferManager} object is disabled.
622      */
isDisabled()623     public boolean isDisabled() {
624         return mDisabled;
625     }
626 
627     /**
628      * Returns if {@link BufferManager} has checked the write speed,
629      * which is suitable for Trickplay.
630      */
631     @VisibleForTesting
hasSpeedCheckDone()632     public boolean hasSpeedCheckDone() {
633         return mSpeedCheckCount > 0;
634     }
635 
636     /**
637      * Sets minimum sample size for write speed check.
638      * @param sampleSize minimum sample size for write speed check.
639      */
640     @VisibleForTesting
setMinimumSampleSizeForSpeedCheck(int sampleSize)641     public void setMinimumSampleSizeForSpeedCheck(int sampleSize) {
642         mMinSampleSizeForSpeedCheck = sampleSize;
643     }
644 }
645