tracesrv/tracecore/btrace_handler/src/TraceCoreMediaWriter.cpp
changeset 56 aa2539c91954
equal deleted inserted replaced
54:a151135b0cf9 56:aa2539c91954
       
     1 // Copyright (c) 2007-2010 Nokia Corporation and/or its subsidiary(-ies).
       
     2 // All rights reserved.
       
     3 // This component and the accompanying materials are made available
       
     4 // under the terms of "Eclipse Public License v1.0"
       
     5 // which accompanies this distribution, and is available
       
     6 // at the URL "http://www.eclipse.org/legal/epl-v10.html".
       
     7 //
       
     8 // Initial Contributors:
       
     9 // Nokia Corporation - initial contribution.
       
    10 //
       
    11 // Contributors:
       
    12 //
       
    13 // Description:
       
    14 // Trace Core
       
    15 //
       
    16 
       
    17 #include <kernel/kernel.h>
       
    18 #include <nkern/nk_priv.h> // Symbian internal -> Needed for kernel lock and NThread mutex state
       
    19 
       
    20 #include "TraceCoreMediaWriter.h" 
       
    21 #include "OstTraceDefinitions.h"
       
    22 #ifdef OST_TRACE_COMPILER_IN_USE
       
    23 #include "TraceCoreMediaWriterTraces.h"
       
    24 #endif
       
    25 
       
    26 
       
    27 
       
    28 //- Internal definitions ----------------------------------------------------
       
    29 #ifdef MEDIA_WRITER_STATISTICS
       
    30 
       
    31 /**
       
    32  * Collects statistics about the media writer
       
    33  */
       
    34 class TMediaWriterStatistics
       
    35     {
       
    36 public:
       
    37     TUint32 iTraces;                // Number of traces received
       
    38     TUint32 iSent;                  // Number of traces sent
       
    39     TUint32 iFailed;                // Number of traces failed to send
       
    40     TUint32 iMissed;                // Number of traces missed due to not enough buffers
       
    41     TUint32 iSleepCount;            // Number of times the threads have slept waiting for buffers
       
    42     TUint32 iCannotSleepCount;      // Number of times a thread could not be slept when it should
       
    43     TUint32 iBlockCount;            // Number of times a thread has been blocked waiting for buffers
       
    44     TUint32 iCannotBlockCount;      // Number of times a thread could not be blocked when it should
       
    45     TUint32 iWaitDidNotHelp;        // Number of waits that did not help
       
    46     TUint32 iMinFreeBuffers;        // Minimum number of free buffers since boot
       
    47     TUint32 iInvalidContextTrace;   // Number of IRQ / IDFC traces ignored
       
    48     TUint32 iIdlePings;             // Statistics are written after some idle time
       
    49     TBool iStatisticsChanged;       // Flags indicating change in statistics
       
    50     };
       
    51 
       
    52 /**
       
    53  * Macro to wrap statistics-related code
       
    54  */
       
    55 #define STATS( x ) x
       
    56     
       
    57 /**
       
    58  * Initializes the statistics structure
       
    59  */
       
    60 TBool DTraceCoreMediaWriter::InitStatistics()
       
    61     {
       
    62     iStatistics = new TMediaWriterStatistics();
       
    63     return iStatistics != NULL;
       
    64     }
       
    65 
       
    66 /**
       
    67  * Number of idle timer pings before sending statistics
       
    68  */
       
    69 const TUint KPingsBeforeStatistics = 640;
       
    70 
       
    71 #else
       
    72 
       
    73 /**
       
    74  * Empty class for statistics if flag is not set
       
    75  */
       
    76 class TMediaWriterStatistics {};
       
    77 
       
    78 #define STATS( x )
       
    79     
       
    80 #endif
       
    81 
       
    82 
       
    83 /**
       
    84  * Max length of one trace
       
    85  */
       
    86 const TUint32 KMaxBufLength = 256;
       
    87 
       
    88 /**
       
    89  * Number of traces that can be buffered
       
    90  */
       
    91 const TUint32 KBufCount = 2048;
       
    92 
       
    93 /**
       
    94  * Send timer ping frequency in milliseconds
       
    95  */
       
    96 const TUint KTimerPing = 10000;
       
    97 
       
    98 /**
       
    99  * Starts to sleep threads when lots of buffers are in use
       
   100  */
       
   101 const TUint32 KSleepBufLimit = 512;
       
   102 
       
   103 /**
       
   104  * Ticks to sleep when using NKern::Sleep
       
   105  */
       
   106 const TUint32 KSleepTicks = 1;
       
   107 
       
   108 /**
       
   109  * Ticks to sleep when using Kern::NanoWait
       
   110  */
       
   111 const TUint32 KNanoWaitTicks = 10000;
       
   112 
       
   113 /**
       
   114  * Starts to block threads when running out of buffers
       
   115  */
       
   116 const TUint32 KBlockBufLimit = 32;
       
   117 
       
   118 /**
       
   119  * Maximum number of loops to run when blocking
       
   120  */
       
   121 const TInt KMaxBlockLoops = 100;
       
   122 
       
   123 
       
   124 /**
       
   125  * Constructor
       
   126  */
       
   127 EXPORT_C DTraceCoreMediaWriter::DTraceCoreMediaWriter()
       
   128 : DTraceCoreWriter( EWriterTypeUSBPhonet )
       
   129 , iSendDfc( DTraceCoreMediaWriter::SendDfc, this, DTraceCore::GetActivationQ()), KDefaultDfcPriority )
       
   130 , iSendTimerActive( EFalse )
       
   131 , iFirstFreeBuffer( NULL )
       
   132 , iFirstReadyBuffer( NULL )
       
   133 , iLastReadyBuffer( NULL )
       
   134 , iTraceBuffers( NULL )
       
   135 , iFreeBuffers( KBufCount )
       
   136 , iSenderThread( NULL )
       
   137 , iLastTraceSent( 0 )
       
   138 , iStatistics( NULL )
       
   139     {
       
   140     // No implementation in constructor 
       
   141     }
       
   142 
       
   143 
       
   144 /**
       
   145  * Destructor
       
   146  */
       
   147 EXPORT_C DTraceCoreMediaWriter::~DTraceCoreMediaWriter()
       
   148     {
       
   149     
       
   150     // Delete all trace buffers
       
   151     for ( int i = 0; i < KBufCount; i++ )
       
   152         {
       
   153         delete[] iTraceBuffers[ i ].iBuffer;
       
   154         }
       
   155     
       
   156     delete[] iTraceBuffers;
       
   157     delete iStatistics;
       
   158     }
       
   159 
       
   160 
       
   161 /**
       
   162  * Registers this writer to TraceCore
       
   163  * 
       
   164  * @return KErrNone if successful
       
   165  */
       
   166 EXPORT_C TInt DTraceCoreMediaWriter::Register()
       
   167     {
       
   168     // Media writer uses the media interface from SendReceive
       
   169     TInt retval( KErrGeneral );
       
   170     
       
   171     // Get TraceCore
       
   172     DTraceCore* traceCore = DTraceCore::GetInstance();
       
   173     if ( traceCore != NULL )
       
   174         {
       
   175         
       
   176         // Get MediaWriter interface
       
   177         DTraceCoreMediaIf* writerIf = traceCore->GetSendReceive().GetMediaWriterIf();
       
   178         if ( writerIf != NULL )
       
   179             {
       
   180             // Allocates memory for the trace buffers
       
   181             TBool memOK = ETrue;
       
   182             STATS( memOK = InitStatistics() );
       
   183             if (memOK)
       
   184                 {
       
   185                 iTraceBuffers = new TTraceBuffer[ KBufCount ];
       
   186                 }
       
   187             if ( iTraceBuffers == NULL )
       
   188                 {
       
   189                 memOK = EFalse;
       
   190                 }
       
   191             if ( memOK  )
       
   192                 {
       
   193                 for ( int i = 0; i < KBufCount && memOK; i++ )
       
   194                     {
       
   195                     iTraceBuffers[ i ].iBuffer = new TUint8[ KMaxBufLength ];
       
   196                     if (iTraceBuffers[ i ].iBuffer == NULL)
       
   197                         {
       
   198                         memOK = EFalse;
       
   199                         }
       
   200                     if ( memOK )
       
   201                         {
       
   202                         // Initially all buffers are linked to iFirstFreeBuffer
       
   203                         if ( i < ( KBufCount - 1 ) )
       
   204                             {
       
   205                             iTraceBuffers[ i ].iNext = &( iTraceBuffers[ i + 1 ] );
       
   206                             }
       
   207                         }
       
   208                     else
       
   209                         {
       
   210                         memOK = EFalse;
       
   211                         }
       
   212                     }
       
   213                 }
       
   214             else
       
   215                 {
       
   216                 memOK = EFalse;
       
   217                 }
       
   218             
       
   219             // Memory was allocated succesfully
       
   220             if ( memOK )
       
   221                 {
       
   222                 iFastCounterBetweenTraces = NKern::FastCounterFrequency() / GetTraceFrequency();
       
   223                 iFirstFreeBuffer = iTraceBuffers;
       
   224                 
       
   225                 // Register to the TraceCore
       
   226                 retval = DTraceCoreWriter::Register();
       
   227                 if ( retval == KErrNone )
       
   228                     {
       
   229                     OstTrace0( TRACE_NORMAL, DTRACECOREMEDIAWRITER_REGISTER_WRITER_REGISTERED,
       
   230                     		"DTraceCoreMediaWriter::Register - Media writer registered");
       
   231                     iMediaIf = writerIf;
       
   232                     }
       
   233                 }
       
   234             else
       
   235                 {
       
   236                 OstTrace0( TRACE_IMPORTANT , DTRACECOREMEDIAWRITER_REGISTER_MEM_ALLOC_FAILED,
       
   237                 		"DTraceCoreMediaWriter::Register - Failed to allocate memory");
       
   238                 retval = KErrNoMemory;
       
   239                 }
       
   240             }
       
   241         }
       
   242     OstTrace1( TRACE_BORDER, DTRACECOREMEDIAWRITER_REGISTER_EXIT, "< DTraceCoreMediaWriter::Register. Ret:%d", retval );
       
   243     return retval;
       
   244     }
       
   245 
       
   246 
       
   247 /*
       
   248  * Timer callback enques the DFC
       
   249  * 
       
   250  * @param aMediaWriter the media writer
       
   251  */
       
   252 void DTraceCoreMediaWriter::SendTimerCallback( TAny* aMediaWriter )
       
   253     {
       
   254     DTraceCoreMediaWriter* writer = static_cast< DTraceCoreMediaWriter* >( aMediaWriter );
       
   255     writer->SendTimerCallback();
       
   256     }
       
   257 
       
   258 
       
   259 /*
       
   260  * Called from the static timer callback function
       
   261  */
       
   262 void DTraceCoreMediaWriter::SendTimerCallback()
       
   263     {
       
   264     NKern::Lock();
       
   265     if ( iFreeBuffers < KBufCount )
       
   266         {
       
   267         STATS( iStatistics->iIdlePings = 0 );
       
   268         
       
   269         // Kernel locked, can call DoEnque
       
   270         iSendDfc.DoEnque(); 
       
   271         }
       
   272     else
       
   273         {
       
   274         STATS( iStatistics->iIdlePings++ );
       
   275 #ifndef MEDIA_WRITER_STATISTICS
       
   276         // The callback timer is stopped unless writing statistics
       
   277         iSendTimer.Cancel();
       
   278         iSendTimerActive = EFalse;
       
   279 #endif
       
   280         }
       
   281     NKern::Unlock();
       
   282 #ifdef MEDIA_WRITER_STATISTICS
       
   283     if ( iStatistics->iIdlePings == KPingsBeforeStatistics && iStatistics->iStatisticsChanged )
       
   284         {
       
   285         
       
   286         // Print out trace statistics
       
   287         OstTraceExt4( TRACE_INTERNAL, DTRACECOREMEDIAWRITER_STATISTICS,
       
   288         		"DTraceCoreMediaWriter - Statistics. Traces:%u Missed:%u Sent:%u Failed:%u",
       
   289         		iStatistics->iTraces, iStatistics->iMissed, iStatistics->iSent, iStatistics->iFailed );
       
   290         
       
   291         // Print out sleep / wait statistics
       
   292         OstTraceExt4( TRACE_INTERNAL, DTRACECOREMEDIAWRITER_SLEEPS,
       
   293         		"DTraceCoreMediaWriter - Sleep / wait statistics. Sleeps:%u CannotSleeps:%u Blocks:%u CannotBlocks:%u",
       
   294         		iStatistics->iSleepCount, iStatistics->iCannotSleepCount, iStatistics->iBlockCount,
       
   295         		iStatistics->iCannotBlockCount );
       
   296         
       
   297         // Print out timer values
       
   298         OstTraceExt4( TRACE_INTERNAL, DTRACECOREMEDIAWRITER_TIMERS,
       
   299         		"DTraceCoreMediaWriter - Timer values %d %d %d %d",
       
   300         		(TInt)NKern::TickCount(), (TInt)NKern::FastCounter(), (TInt)NKern::TickPeriod(),
       
   301         		(TInt)NKern::FastCounterFrequency() );
       
   302         
       
   303         // Print out misc statistics
       
   304         OstTraceExt3( TRACE_INTERNAL, DTRACECOREMEDIAWRITER_MISC,
       
   305         		"DTraceCoreMediaWriter - Misc statistics WaitDidntHelp:%u MinFreeBuffers:%u InvalidContextTraces%u",
       
   306         		iStatistics->iWaitDidNotHelp, iStatistics->iMinFreeBuffers, iStatistics->iInvalidContextTrace );
       
   307         iStatistics->iStatisticsChanged = EFalse;
       
   308         }
       
   309 #endif
       
   310     }
       
   311 
       
   312 
       
   313 /**
       
   314  * Starts an entry
       
   315  * 
       
   316  * @param aType the type of the trace entry
       
   317  * @return the entry ID that is passed to other Write-functions
       
   318  */
       
   319 EXPORT_C TUint32 DTraceCoreMediaWriter::WriteStart( TWriterEntryType aType )
       
   320     {
       
   321     // Detaches the first free buffer from the free buffers list
       
   322     TInt context( NKern::CurrentContext() );
       
   323     TTraceBuffer* freeBuf = NULL;
       
   324     STATS( iStatistics->iStatisticsChanged = ETrue );
       
   325     if ( context == NKern::EThread )
       
   326         {
       
   327         NKern::Lock();
       
   328         freeBuf = iFirstFreeBuffer;
       
   329         if ( freeBuf != NULL )
       
   330             {
       
   331             iFirstFreeBuffer = freeBuf->iNext;
       
   332             freeBuf->iNext = NULL;
       
   333             freeBuf->iMissedBefore = 0;
       
   334             iFreeBuffers--;
       
   335             STATS( iStatistics->iMinFreeBuffers = ( iFreeBuffers < iStatistics->iMinFreeBuffers )
       
   336                     ? iFreeBuffers : iStatistics->iMinFreeBuffers );
       
   337             // Timer is activated and stays active until all buffers are empty
       
   338             if ( !iSendTimerActive )
       
   339                 {
       
   340                 iSendTimer.Periodic( KTimerPing, SendTimerCallback, this );
       
   341                 iSendTimerActive = ETrue;
       
   342                 }
       
   343             }
       
   344         else
       
   345             {
       
   346             iFirstReadyBuffer->iMissedBefore++;
       
   347             STATS( iStatistics->iMissed++ );
       
   348             }
       
   349         NKern::Unlock();
       
   350         if ( freeBuf != NULL )
       
   351             {
       
   352             StartBuffer( aType, *freeBuf );
       
   353             }
       
   354         }
       
   355     else
       
   356         {
       
   357         STATS( iStatistics->iInvalidContextTrace++ );
       
   358         freeBuf = NULL;
       
   359         }
       
   360     return ( TUint32 )freeBuf;
       
   361     }
       
   362 
       
   363 
       
   364 /**
       
   365  * Ends an entry
       
   366  *
       
   367  * @param aEntryId the entry ID returned by WriteStart
       
   368  */
       
   369 EXPORT_C void DTraceCoreMediaWriter::WriteEnd( TUint32 aEntryId )
       
   370     {
       
   371     if ( aEntryId != 0 )
       
   372         {
       
   373         TTraceBuffer* buf = ( TTraceBuffer* )aEntryId;
       
   374         EndBuffer( *buf );
       
   375         // If there's no existing ready buffers, the new buffer is set as first.
       
   376         // Otherwise, it is assigned to the end of the ready buffers queue
       
   377         NKern::Lock();
       
   378         if ( iFirstReadyBuffer == NULL )
       
   379             {
       
   380             iFirstReadyBuffer = buf;
       
   381             }
       
   382         else
       
   383             {
       
   384             iLastReadyBuffer->iNext = buf;
       
   385             }
       
   386         iLastReadyBuffer = buf;
       
   387         STATS( iStatistics->iTraces++ );
       
   388         NKern::Unlock();
       
   389         // Queues the DFC that sends the trace out. The DFC checks the timestamp of the 
       
   390         // last trace sent, so the trace might not be immediately sent out.
       
   391         iSendDfc.Enque();
       
   392 
       
   393         // If the number of buffers is below the sleep limit, this thread is slept for a while.
       
   394         // If the number of buffers is below the block limit after sleep, 
       
   395         // this thread is slept for longer time
       
   396         // Pre-conditions for Sleep:
       
   397         //  - Sleeping thread must not be the one sending traces out
       
   398         //  - Kernel must not be locked
       
   399         //  - Fast mutex must not be held by the thread
       
   400         if ( iFreeBuffers < KSleepBufLimit )
       
   401             {
       
   402             NThread* thread( NKern::CurrentThread() );
       
   403             if ( thread != iSenderThread && thread->iHeldFastMutex == NULL && !TScheduler::Ptr()->iKernCSLocked )
       
   404                 {
       
   405                 STATS( iStatistics->iSleepCount++ );
       
   406                 // User-side threads can use NKern::Sleep
       
   407 #ifdef __WINS__
       
   408                 TInt type = 0;
       
   409 #else
       
   410                 TInt type = NKern::CurrentThread()->UserContextType();
       
   411 #endif
       
   412                 Block( type );
       
   413                 if ( iFreeBuffers < KBlockBufLimit )
       
   414                     {
       
   415                     STATS( iStatistics->iBlockCount++ );
       
   416                     TInt loops = 0;
       
   417                     while ( ( iFreeBuffers < KBlockBufLimit ) && ( ++loops ) < KMaxBlockLoops )
       
   418                         {
       
   419                         iSendDfc.Enque();
       
   420                         Block( type );
       
   421                         }
       
   422                     if ( loops == KMaxBlockLoops )
       
   423                         {
       
   424                         STATS( iStatistics->iWaitDidNotHelp++ );
       
   425                         }
       
   426                     }
       
   427                 }
       
   428             else
       
   429                 {
       
   430                 STATS( iStatistics->iCannotSleepCount++ );
       
   431                 if ( iFreeBuffers < KBlockBufLimit )
       
   432                     {
       
   433                     STATS( iStatistics->iCannotBlockCount++ );
       
   434                     }
       
   435                 }
       
   436             }
       
   437         }
       
   438     }
       
   439 
       
   440 
       
   441 /**
       
   442  * Blocks the current thread for a while
       
   443  * 
       
   444  * @param aType the context type from NThread::UserContextType
       
   445  */
       
   446 void DTraceCoreMediaWriter::Block( TInt aType )
       
   447     {
       
   448 #ifdef __WINS__
       
   449     ( void )aType;
       
   450     NKern::Sleep( KSleepTicks );
       
   451 #else
       
   452     if ( aType == NThread::EContextExec )
       
   453         {
       
   454         NKern::Sleep( KSleepTicks );
       
   455         }
       
   456     else
       
   457         {
       
   458         Kern::NanoWait( KNanoWaitTicks );
       
   459         }
       
   460 #endif
       
   461     }
       
   462 
       
   463 
       
   464 /**
       
   465  * Writes 8-bit data to given entry
       
   466  * 
       
   467  * @param aEntryId the entry ID returned by WriteStart
       
   468  * @param aData the trace data
       
   469  */
       
   470 EXPORT_C void DTraceCoreMediaWriter::WriteData( TUint32 aEntryId, TUint8 aData )
       
   471     {
       
   472     if ( aEntryId != 0 )
       
   473         {
       
   474         TTraceBuffer* buf = ( TTraceBuffer* )aEntryId;
       
   475         if ( buf->iLength < KMaxBufLength )
       
   476             {
       
   477             *( buf->iBuffer + buf->iLength ) = aData;
       
   478             buf->iLength++;
       
   479             }
       
   480         }
       
   481     }
       
   482 
       
   483 
       
   484 /**
       
   485  * Writes 16-bit data to given entry
       
   486  * 
       
   487  * @param aEntryId the entry ID returned by WriteStart
       
   488  * @param aData the trace data
       
   489  */
       
   490 EXPORT_C void DTraceCoreMediaWriter::WriteData( TUint32 aEntryId, TUint16 aData )
       
   491     {
       
   492     if ( aEntryId != 0 )
       
   493         {
       
   494         TTraceBuffer* buf = ( TTraceBuffer* )aEntryId;
       
   495         if ( buf->iLength + sizeof ( TUint16 ) <= KMaxBufLength )
       
   496             {
       
   497             *( ( TUint16* )( buf->iBuffer + buf->iLength ) ) = aData;
       
   498             buf->iLength += sizeof ( TUint16 );
       
   499             }
       
   500         }
       
   501     }
       
   502 
       
   503 
       
   504 /**
       
   505  * Writes 32-bit data to given entry
       
   506  * 
       
   507  * @param aEntryId the entry ID returned by WriteStart
       
   508  * @param aData the trace data
       
   509  */
       
   510 EXPORT_C void DTraceCoreMediaWriter::WriteData( TUint32 aEntryId, TUint32 aData )
       
   511     {
       
   512     if ( aEntryId != 0 )
       
   513         {
       
   514         TTraceBuffer* buf = ( TTraceBuffer* )aEntryId;
       
   515         if ( buf->iLength + sizeof ( TUint32 ) <= KMaxBufLength )
       
   516             {
       
   517             *( ( TUint32* )( buf->iBuffer + buf->iLength ) ) = SWAP_DATA( aData );
       
   518             buf->iLength += sizeof ( TUint32 );
       
   519             }
       
   520         }
       
   521     }
       
   522 
       
   523 
       
   524 /**
       
   525  * DFC for sending data
       
   526  * 
       
   527  * @param aMediaWriter the media writer
       
   528  */
       
   529 void DTraceCoreMediaWriter::SendDfc( TAny* aMediaWriter )
       
   530     {
       
   531     DTraceCoreMediaWriter* writer = static_cast< DTraceCoreMediaWriter* >( aMediaWriter );
       
   532     writer->SendDfc();
       
   533     }
       
   534 
       
   535 
       
   536 /**
       
   537  * Called from static DFC function to send a buffer
       
   538  */
       
   539 void DTraceCoreMediaWriter::SendDfc()
       
   540     {
       
   541     if ( iSenderThread == NULL )
       
   542         {
       
   543         iSenderThread = NKern::CurrentThread();
       
   544         }
       
   545     TUint32 time = NKern::FastCounter();
       
   546     // Timestamp is checked so that this does not send too frequently
       
   547     // Otherwise the USB Phonet Link will crash
       
   548     // FastCounter may overflow, so the less than check is also needed
       
   549     if ( time > ( iLastTraceSent + iFastCounterBetweenTraces ) || ( time < iLastTraceSent ) )
       
   550         {
       
   551         // Gets the first buffer that is ready for sending.
       
   552         // Assigns the next buffer as first ready buffer
       
   553         NKern::Lock();
       
   554         TTraceBuffer* buf = iFirstReadyBuffer;
       
   555         if ( buf != NULL )
       
   556             {
       
   557             iFirstReadyBuffer = buf->iNext;
       
   558             }
       
   559         NKern::Unlock();
       
   560         if ( buf != NULL )
       
   561             {
       
   562             TPtrC8 ptr( buf->iBuffer, buf->iLength );
       
   563             TInt res = iMediaIf->SendTrace( ptr );
       
   564             // After sending the buffer is moved to the free buffers list
       
   565             NKern::Lock();
       
   566             if ( res != KErrNone )
       
   567                 {
       
   568                 STATS( iStatistics->iFailed++ );
       
   569                 if ( iFirstReadyBuffer != NULL )
       
   570                     {
       
   571                     iFirstReadyBuffer->iMissedBefore += buf->iMissedBefore;
       
   572                     }
       
   573                 }
       
   574             else
       
   575                 {
       
   576                 STATS( iStatistics->iSent++ );
       
   577                 }
       
   578             buf->iNext = iFirstFreeBuffer;
       
   579             iFirstFreeBuffer = buf;
       
   580             iFreeBuffers++;
       
   581             NKern::Unlock();
       
   582             iLastTraceSent = time;
       
   583             }
       
   584         }
       
   585     }
       
   586 
       
   587 // End of File