/*++ Copyright (c) 1991 Microsoft Corporation Module Name: buffdesc.c Abstract: Routines for allocating, managing and freeing buffer descriptors. Author: Garth Conboy Initial Coding Nikhil Kamkolkar Rewritten for microsoft coding style. mpized Revision History: GC - (04/05/92): When using "on board" data, make sure the real "in use" portion is at the END of the "on board" chunk! GC - (06/24/92): Lotsa changes; fully documented in BuffDesc.h. The two main additions are that a BufferDescriptor chunk may now contain BOTH on-board and out-board data, and the out-board buffer may now be "opaque" (some system dependend format that we don't understand) rather than simply a "char *" buffer. --*/ #define IncludeBuffDescErrors 1 #include "atalk.h" #define VerboseMesages 0 #define DebugCode 0 #if Verbose or (Iam a Primos) static long totalChunksAllocated; static long totalChunksFreed; #endif ExternForVisibleFunction BufferDescriptor FindFreeDescriptor(long size); // // Allocate and attach a new header to an existing buffer chain. The data // will be marked as free-able. // BufferDescriptor far AllocateHeader(BufferDescriptor chain, long size) { BufferDescriptor newDescriptor; // // Check for the various cases that we can add (prepend) to the first // chunk in the passed chain. // if (chain isnt Empty) { // Handle the "prependInPlace" case. See comments in "buffdesc.h". if (chain->outBoardDataValid and chain->prependInPlace) { if (chain->freeData or chain->opaqueData) { // // The prependInPlace bit should never be set in a case that we've // allocated the buffer (we don't reserve any space preceding the // buffer for a "real prepend") or when we're just describing some // sort of opaque thing. // ErrorLog("AllocateHeader", ISevError, __LINE__, UnknownPort, IErrBuffDescBadRealPrepend, IMsgBuffDescBadRealPrepend, Insert0()); FreeBufferChain(chain); return(Empty); } // // Just modify the out-board existing chunk; "back up" the requested // amount. // chain->outBoardBuffer -= size; chain->outBoardData = chain->outBoardBuffer; chain->outBoardSize += size; chain->outBoardAllocatedSize += size; chain->data = chain->outBoardData; return(chain); } // // Is there room preceeding the "in-use" portion of the existing out-board // buffer? Note that we can only do this if only the out-board chunk // is in use (i.e. doesn't already have an on-board header prepended). // if (chain->outBoardDataValid and not chain->onBoardDataValid and (chain->outBoardAllocatedSize - chain->outBoardSize) >= size) { chain->outBoardData -= size; chain->outBoardSize += size; chain->data = chain->outBoardData; return(chain); } // // Can we use the on-board space? First check the case that the on-board // buffer space has not be used at all yet (not valid yet). // if (not chain->onBoardDataValid and size <= MaxOnBoardBytes) { chain->onBoardDataValid = True; chain->onBoardData = chain->onBoardBuffer + (MaxOnBoardBytes - size); chain->onBoardSize = size; chain->data = chain->onBoardData; return(chain); } // // Now check the case that the on-board buffer space is in use, but // has sufficient space available for a new on-board header. // if (chain->onBoardDataValid and (MaxOnBoardBytes - chain->onBoardSize) >= size) { chain->onBoardData -= size; chain->onBoardSize += size; chain->data = chain->onBoardData; return(chain); } } // // Try to find a free buffer descriptor, otherwise, allocate one. If // both fail, run away. Don't bother logging an error... our caller needs // to check for this (and can give a more exacting error message). // if ((newDescriptor = FindFreeDescriptor(size)) is Empty) newDescriptor = (BufferDescriptor)Calloc(sizeof(*newDescriptor), 1); if (newDescriptor is Empty) { FreeBufferChain(chain); return(Empty); } // // If we've gotten a lone descriptor, we either need to allocate an out- // board buffer or we can use the "on board" space. // if (not newDescriptor->outBoardDataValid) { if (size <= MaxOnBoardBytes) { newDescriptor->onBoardDataValid = True; newDescriptor->onBoardData = newDescriptor->onBoardBuffer + (MaxOnBoardBytes - size); newDescriptor->onBoardSize = size; newDescriptor->data = newDescriptor->onBoardData; } else { if ((newDescriptor->outBoardBuffer = (char far *)Malloc(size)) is Empty) { FreeBufferChain(newDescriptor); FreeBufferChain(chain); return(Empty); } newDescriptor->freeData = True; newDescriptor->outBoardDataValid = True; newDescriptor->outBoardData = newDescriptor->outBoardBuffer; newDescriptor->outBoardSize = newDescriptor->outBoardAllocatedSize = size; newDescriptor->data = newDescriptor->outBoardData; } } else { // // Okay, we've gotten a descriptor complete with an existing out-board // buffer, use the tail end of it. // newDescriptor->outBoardData = newDescriptor->outBoardBuffer + (newDescriptor->outBoardAllocatedSize - size); newDescriptor->outBoardSize = size; newDescriptor->data = newDescriptor->outBoardData; } #if Verbose or (Iam a Primos) totalChunksAllocated += 1; #endif // // Okay, we have a buffer descriptor that passes the tests, prepend it // to the existing chain, and return it. // newDescriptor->next = chain; return(newDescriptor); } // AllocateHeader // // Attach an existing chunk of data to an exisitng buffer chain. The data // will be marked as non-free-able. // BufferDescriptor far AttachHeader(BufferDescriptor chain, long size, char far *data, Boolean opaqueData) { BufferDescriptor newDescriptor; // // Try to find a free lone descriptor, otherwise allocate one, if both fail // return empty handed. // if ((newDescriptor = FindFreeDescriptor(0)) is Empty) newDescriptor = (BufferDescriptor)Calloc(sizeof(*newDescriptor), 1); if (newDescriptor is Empty) { FreeBufferChain(chain); return(Empty); } // Attach the passed data, and set the size. newDescriptor->outBoardDataValid = True; newDescriptor->outBoardData = newDescriptor->outBoardBuffer = data; newDescriptor->outBoardSize = newDescriptor->outBoardAllocatedSize = size; newDescriptor->freeData = False; newDescriptor->opaqueData = opaqueData; newDescriptor->data = newDescriptor->outBoardData; #if Verbose or (Iam a Primos) totalChunksAllocated += 1; #endif // Prepend to passed chain and return the new head. newDescriptor->next = chain; return(newDescriptor); } // AttachHeader // Compute the actual, in use, data size of a buffer chain (bytes). extern long far SizeOfBufferChain(BufferDescriptor chain) { long size = 0; for ( ; chain isnt Empty; chain = chain->next) size += (chain->outBoardSize + chain->onBoardSize); return(size); } // SizeOfBufferChain // // Given a multi-chunk buffer chain, coalesce it into a single chunk // chain and free the original. // extern BufferDescriptor far CoalesceBufferChain(BufferDescriptor chain) { BufferDescriptor coalesced; coalesced = CopyBufferChain(chain); FreeBufferChain(chain); return(coalesced); } // CoalesceBufferChain // // Create a copy of a buffer descriptor (basically the same as // CoalesceBufferChain, but don't free the orignal). // extern BufferDescriptor far CopyBufferChain(BufferDescriptor chain) { BufferDescriptor copy, chunk; char far *data; #if 0 extern void DumpBufferDescriptorInfo(void); DumpBufferDescriptorInfo(); #endif // Allocate a new chunk large enough to hold the coalesced chain. if ((copy = NewBufferDescriptor(SizeOfBufferChain(chain))) is Empty) { FreeBufferChain(chain); return(Empty); } // Walk the chain, copying the data into the new single chunk. for (data = copy->data, chunk = chain; chunk isnt Empty; chunk = chunk->next) { if (chunk->onBoardDataValid) { MoveMem(data, chunk->onBoardData, chunk->onBoardSize); data += chunk->onBoardSize; } if (chunk->outBoardDataValid) { MoveOutBoardData(data, chunk, (long)0, chunk->outBoardSize); data += chunk->outBoardSize; } } // All set. return(copy); } // CopyBufferChain // Free a buffer chain and its assocciated free-able data. void far FreeBufferChain(BufferDescriptor chain) { BufferDescriptor next; // Walk the chain either freeing or adding to the free list. for ( ; chain isnt Empty; chain = next) { next = chain->next; // // Logically "free" the on-board data. Make things look a little // cleaner than they really need to be. // chain->onBoardDataValid = False; chain->onBoardSize = 0; chain->onBoardData = Empty; chain->data = Empty; chain->transmitCompleteHandler = Empty; // // If we have data to free (that's larger than we can keep) free it // and convert the descriptor to lone one. If we're not freeing (or // keeping) the data, make a lone descriptor. // if (chain->outBoardDataValid) { // // Note that the following if/else will leave outBoardDataValid and // freeData either both True or both False. // if (chain->freeData and chain->outBoardAllocatedSize > MaxKeptBufferBytes) { Free(chain->outBoardBuffer); chain->outBoardDataValid = False; chain->freeData = False; } else if (not chain->freeData) chain->outBoardDataValid = False; if (chain->freeOpaqueDataDescriptor) FreeOpaqueDataDescriptor(chain->outBoardBuffer); // Clean up. if (not chain->outBoardDataValid) { // Not saving any out-board data. chain->outBoardBuffer = Empty; chain->outBoardAllocatedSize = 0; } chain->outBoardSize = 0; chain->outBoardData = Empty; chain->opaqueData = False; chain->prependInPlace = False; chain->freeOpaqueDataDescriptor = False; } // // If we can keep another descriptor on our free list, add it. Otherwise, // free any data and the descriptor too. // EnterCriticalSection(); if (freeBufferDescriptorCount < MaxFreeBufferDescriptors) { chain->next = freeBufferDescriptorList; freeBufferDescriptorList = chain; freeBufferDescriptorCount += 1; LeaveCriticalSection(); } else { LeaveCriticalSection(); if (chain->outBoardDataValid and chain->freeData) Free(chain->outBoardBuffer); Free(chain); #if VerboseMessages ErrorLog("FreeBufferChain", ISevError, __LINE__, UnknownPort, IErrBuffDescFreeingDescriptor, IMsgBuffDescFreeingDescriptor, Insert0()); #endif } #if Verbose or (Iam a Primos) totalChunksFreed += 1; #endif } } // FreeBufferChain // // Compute the Ddp checksum of a buffer chain. // // NOTE BENE: It is BIG performance win to recode this routine in assembly // unless the target environment has a VERY good optimizing // compiler. // // The leadingBytesToSkip is assumed to be within ONE header // (either on-board or out-board data). // // short unsigned far DdpChecksumBufferChain(BufferDescriptor chain, long actualLength, long leadingBytesToSkip) { short unsigned checksum = 0; long bytesProcessed = leadingBytesToSkip; long index; char c; // // This checksum algorithum is from Inside AppleTalk page IV-5; designed, // no doubt, to be slow on any 32-bit machine. Sigh. // for ( ; chain isnt Empty and bytesProcessed < actualLength; chain = chain->next) { // First do any on-board data in this chunk. if (chain->onBoardDataValid) { for (index = leadingBytesToSkip; index < chain->onBoardSize and bytesProcessed < actualLength; index += 1, bytesProcessed += 1) { checksum += (unsigned char)chain->onBoardData[index]; if (checksum & 0x8000) // 16-bit rotate left one bit... { checksum <<= 1; checksum += 1; } else checksum <<= 1; } leadingBytesToSkip = 0; } // Then do any out-board data in this chunk. if (chain->outBoardDataValid) { for (index = leadingBytesToSkip; index < chain->outBoardSize and bytesProcessed < actualLength; index += 1, bytesProcessed += 1) { if (not chain->opaqueData) checksum += (unsigned char)chain->outBoardData[index]; else { // // !!!!! This will perform horrendously slowly and // should be optimized in some system dependent // way. // !!!!! // MoveFromOpaque(&c, chain->outBoardData, index, 1); checksum += (unsigned char)c; } if (checksum & 0x8000) // 16-bit rotate left one bit... { checksum <<= 1; checksum += 1; } else checksum <<= 1; } leadingBytesToSkip = 0; } } if (checksum is 0) checksum = 0xFFFF; return(checksum); } // DdpChecksumBufferChain // // Given a buffer descriptor, adjust the most recent header to reflect new // "in use" pointers and sizes. A negative adjustment cause less data to // be considered "in use," a positive adjustment causes more data to be // considered "in use." A negative adjustment must be no larger than the // size of the previous "AllocateHeader." Positive adjustments must be // preceeded by negative adjustments and may not be larger than the sum of // previous negative adjustments. What this means is this routine may only // be used to "move around" in the most recently allocated header and may // not exceed its bounds. This routine will be used only on headers // allocated by the stack -- we don't have to worry about opaque data. // void far AdjustBufferDescriptor(BufferDescriptor descriptor, long adjustment) { long maxSize, newSize; // Validate requested adjustment. if (descriptor->onBoardDataValid) { maxSize = MaxOnBoardBytes; newSize = descriptor->onBoardSize + adjustment; } else { maxSize = descriptor->outBoardAllocatedSize; newSize = descriptor->outBoardSize + adjustment; // We can only do adjustments on headers we've allocated. if (descriptor->opaqueData or not descriptor->freeData) newSize = -1; } if (newSize < 0 or newSize > maxSize) { ErrorLog("AdjustBufferDescriptor", ISevError, __LINE__, UnknownPort, IErrBuffDescBadAdjustment, IMsgBuffDescBadAdjustment, Insert0()); return; } // Okay, do the deed. if (descriptor->onBoardDataValid) { descriptor->onBoardSize = newSize; descriptor->onBoardData -= adjustment; descriptor->data = descriptor->onBoardData; } else { descriptor->outBoardSize = newSize; descriptor->outBoardData -= adjustment; descriptor->data = descriptor->outBoardData; } return; } // AdjustBufferDescriptor // // Given a buffer descriptor chain, create a new one describing a subset of // the original. This routine generally does not copy any data, so the // original buffer descriptor may not be freed before the subset descriptor // is freed. // BufferDescriptor far SubsetBufferDescriptor(BufferDescriptor chain, long offset, long size) { BufferDescriptor newDescriptor; Boolean freeOpaqueDataDescriptor; // // For the "normal case" of subsetting a chain that has only one chunk // and all of its data is out-board, just copy the descriptor and adjust // the pointers. // if (chain->next is Empty and chain->outBoardDataValid and not chain->onBoardDataValid) { // Get a new lone descriptor. if ((newDescriptor = FindFreeDescriptor(0)) is Empty) newDescriptor = (BufferDescriptor)Calloc(sizeof(*newDescriptor), 1); if (newDescriptor is Empty) return(Empty); // Set new values. newDescriptor->outBoardDataValid = True; newDescriptor->opaqueData = chain->opaqueData; newDescriptor->freeData = False; newDescriptor->outBoardAllocatedSize = size; newDescriptor->outBoardSize = size; // Make only the requested subset be described. if (not chain->opaqueData) newDescriptor->outBoardBuffer = chain->outBoardBuffer + offset; else { newDescriptor->outBoardBuffer = SubsetOpaqueDataDescriptor(chain->outBoardBuffer, offset, size, &freeOpaqueDataDescriptor); chain->freeOpaqueDataDescriptor = freeOpaqueDataDescriptor; } newDescriptor->outBoardData = newDescriptor->outBoardBuffer; newDescriptor->data = newDescriptor->outBoardData; return(newDescriptor); } // // Okay, now the slow case! Copy the descriptor and fudge the pointers! // N.B. the copy will be a single chunk in one place, and won't have any // opaque data. // if ((newDescriptor = CopyBufferChain(chain)) is Empty) return(Empty); if (newDescriptor->outBoardDataValid) { newDescriptor->outBoardSize = size; newDescriptor->outBoardData += offset; newDescriptor->data = newDescriptor->outBoardData; } else { newDescriptor->onBoardSize = size; newDescriptor->onBoardData += offset; newDescriptor->data = newDescriptor->onBoardData; } return(newDescriptor); } // SubsetBufferDescriptor // // Move a specified amount of data from "out-board data" to a "char *" // buffer. This routine handles both "char *" and "opaque" out-board // data. // void far MoveOutBoardData(char far *target, BufferDescriptor chunk, long offset, long size) { if (not chunk->opaqueData) { MoveMem(target, chunk->outBoardData + offset, size); return; } // Okay, handle (in a system specific mannor) opaque out-board data. MoveFromOpaque(target, (void far *)chunk->outBoardData, offset, size); return; } // MoveOutBoardData // Move opaque data to a "char *" buffer. void far MoveFromOpaque(char far *target, void far *opaque, long offset, long size) { #if Iam a WindowsNT // // Opaque data is a pointer to an MDL; copy "size" bytes from the // MDL-described data to "target." // CopyDataFromMdlDescribedArea(target, opaque, offset, size); #elif Iam a Primos // Opaque is just "char *" here. MoveMem(target, (char far *)opaque + offset, size); #else // Assume the simple case of opaque just being "char *." MoveMem(target, (char far *)opaque + offset, size); #endif } // MoveFromOpaque // Move a "char *" buffer to opaque data. void far MoveToOpaque(void far *opaque, long offset, char far *buffer, long size) { #if Iam a WindowsNT // // Opaque data is a pointer to an MDL; copy "size" bytes from the // buffer to the MDL-described "target." // CopyDataToMdlDescribedArea(opaque, offset, buffer, size); #elif Iam a Primos // Opaque is just "char *" here. MoveMem((char far *)opaque + offset, buffer, size); #else // Assume the simple case of opaque just being "char *." MoveMem((char far *)opaque + offset, buffer, size); #endif } // MoveToOpaque // // Move an opaque buffer to an opaque buffer. Overlapping areas should // work "correctly" this routine is used to "synch-up" Atp responses. // void far MoveOpaqueToOpaque(void far *targetOpaque, long targetOffset, void far *sourceOpaque, long sourceOffset, long size) { #if Iam a WindowsNT // // Opaque data is pointers to MDLs; copy "size" bytes. Be sure, also, // that overlapping MDL-described area work "correctly." // MoveMdlAreaToMdlArea(targetOpaque, targetOffset, \ sourceOpaque, sourceOffset, size); #elif Iam a Primos // Opaques are just "char *"s here. CarefulMoveMem((char far *)targetOpaque + targetOffset, (char far *)sourceOpaque + sourceOffset, size); #else // Assume the simple case of opaques being just "char *"s here. CarefulMoveMem((char far *)targetOpaque + targetOffset, (char far *)sourceOpaque + sourceOffset, size); #endif } // MoveOpaqueToOpaque // The "strlen()" function for opaque data. size_t far StrlenOpaque(void far *opaque, long offset) { #if Iam a WindowsNT // // Opaque data is a pointer to an MDL; compute a "strlen()" at the // specified offset into the MDL-described target. // return((size_t)StrlenMdlDescribedArea(opaque, offset)); #elif Iam a Primos // // Opaque is just "char *" here. The cast to "size_t" here is to avoid // a Prime CI compiler "loss of precision warning"... the intrinsic // version of "strlen()" is typed to return "long" rather than "long // unsigned"... technically a bug... but, I neither think it's serious // enough, nor do I have enough time, to fix it right now! // return((size_t)strlen((char far *)opaque + offset)); #else // Assume the simple case of opaque just being "char *." return(strlen((char far *)opaque + offset)); #endif } // StrlenOpaque // // Make an opaque data descriptor for a specified "char *" buffer. The // last argument is returned as True if the returned descriptor is a "real // thing" that must be freed when we're done with it (this refers to the // descriptor, not the described data). // // We return Empty if the requested opaque descriptor could not be created; // if Empty is returned, DONT set freeOpaqueDataDescriptor to True. // void far *MakeOpaqueDataDescriptor(char far *buffer, long size, Boolean far *freeOpaqueDataDescriptor) { // // Any of the conditional compilation sections in the routine that set // "*freeOpaqueDataDescriptor" to True will need corresponding sections // in FreeOpaqueDataDescriptor(). // #if Iam a WindowsNT // // Opaque data is pointer to an MDL; we need to create an MDL for // the buffer, so we'll need to free it later. // void *newMdl; *freeOpaqueDataDescriptor = True; newMdl = MakeAnMdl(buffer, size); return(newMdl); #elif Iam a Primos // Here the opaque guys are really just "char *"s. *freeOpaqueDataDescriptor = False; return((void *)buffer); #else // Assume simple "char *" opaques. *freeOpaqueDataDescriptor = False; return((void *)buffer); #endif } // MakeOpaqueDataDescriptor // // Given an opaque data descriptor, create a new opaque data descriptor that // describes a subset of the original -- leave the original unmodified. The // last argument is returned as True if the returned descriptor is a "real // thing" that must be freed when we're done with it (this refers to the // descriptor, not the described data). // // We return Empty if the requested opaque descriptor could not be created; // if Empty is returned, DONT set freeOpaqueDataDescriptor to True. // void far *far SubsetOpaqueDataDescriptor(void far *master, long offset, long size, Boolean far *freeOpaqueDataDescriptor) { // // Any of the conditional compilation sections in the routine that set // "*freeOpaqueDataDescriptor" to True will need corresponding sections // in FreeOpaqueDataDescriptor(). // #if Iam a WindowsNT // // Opaque data is pointer to an MDL; we will need to create a new // MDL to do this operation, so we'll need to free it later. // void *newMdl; *freeOpaqueDataDescriptor = True; newMdl = SubsetAnMdl(master, offset, size); return(newMdl); #elif Iam a Primos // Here the opaque guys are really just "char *"s. *freeOpaqueDataDescriptor = False; return((void *)((char *)master + offset)); #else // Assume simple "char *" opaques. *freeOpaqueDataDescriptor = False; return((void *)((char *)master + offset)); #endif } // SubsetOpaqueDataDescriptor // // Free and opaque data descriptor; this frees the descriptor NOT the data. // This routine will be used to free descriptors that were created by the // stack when "subsetting" passed opaque data descriptors. // void far FreeOpaqueDataDescriptor(void far *descriptor) { #if Iam a WindowsNT // Free the pointed-to MDL (not the described data). FreeAnMdl(descriptor); #endif } // FreeOpaqueDataDescriptor ExternForVisibleFunction BufferDescriptor FindFreeDescriptor(long size) { BufferDescriptor bufferDescriptor; BufferDescriptor previousBufferDescriptor = Empty; BufferDescriptor loneDescriptor = Empty; BufferDescriptor previousLoneDescriptor = Empty; // // If we're looking for a data block bigger than we would have kept, just // look for a lone descriptor (without data). Also if we're looking for // a data block that could fit "on board" the descriptor, just look for // a lone descriptor. // if (size > MaxKeptBufferBytes) size = 0; else if (size <= MaxOnBoardBytes) size = 0; // Walk the free list, looking for a match. EnterCriticalSection(); for (bufferDescriptor = freeBufferDescriptorList; bufferDescriptor isnt Empty; previousBufferDescriptor = bufferDescriptor, bufferDescriptor = bufferDescriptor->next) if (not bufferDescriptor->outBoardDataValid) { // // A lone descriptor, use it if that's what we're looking for, // otherwise, note that we have one that we can fall back on, // if we don't find a better match. // if (size is 0) break; loneDescriptor = bufferDescriptor; previousLoneDescriptor = previousBufferDescriptor; } else if (bufferDescriptor->outBoardDataValid and bufferDescriptor->outBoardAllocatedSize >= size and size isnt 0) break; // A descriptor, with data, that can hold our request! // // If bufferDescriptor isnt Empty, we've either were looking for a lone // descriptor and found one, or we were looking for a descriptor with data // and we found one with enough room for our request. Otherwise, if we've // found a lone descriptor, it's better than nothing for our caller (he // can allocate a data block), so return it. Unlink the chunk from the // list and return it. // if (bufferDescriptor is Empty and loneDescriptor isnt Empty) { bufferDescriptor = loneDescriptor; previousBufferDescriptor = previousLoneDescriptor; } if (bufferDescriptor isnt Empty) { if (previousBufferDescriptor is Empty) freeBufferDescriptorList = bufferDescriptor->next; else previousBufferDescriptor->next = bufferDescriptor->next; if ((freeBufferDescriptorCount -= 1) < 0) { LeaveCriticalSection(); ErrorLog("FindFreeDescriptor", ISevError, __LINE__, UnknownPort, IErrBuffDescBadFreeCount, IMsgBuffDescBadFreeCount, Insert0()); bufferDescriptor->next = Empty; return(bufferDescriptor); } LeaveCriticalSection(); bufferDescriptor->next = Empty; return(bufferDescriptor); } // Oh well, nobody home, return empty handed. LeaveCriticalSection(); return(Empty); } // FindFreeDescriptor #if Verbose or (Iam a Primos) void DumpBufferDescriptorInfo(void) { BufferDescriptor chunk; int freeCount = 0; for (chunk = freeBufferDescriptorList; chunk isnt Empty; chunk = chunk->next) freeCount += 1; printf("\n"); printf("Total chunks allocated = %d.\n", totalChunksAllocated); printf("Total chunks freed = %d.\n", totalChunksFreed); printf("Current chunks in use = %d.\n", totalChunksAllocated - totalChunksFreed); printf("Total chunks on free list = %d (%d).\n", freeBufferDescriptorCount, freeCount); printf("\n"); } // DumpBufferDescriptorInfo #endif