/Public/Src/Utilities/Native/Streams/FileBuffer.cs

https://github.com/Microsoft/BuildXL · C# · 317 lines · 181 code · 49 blank · 87 comment · 40 complexity · 7f5a57b92d3d78c7015b50e94f5d7dfd MD5 · raw file

  1. // Copyright (c) Microsoft Corporation.
  2. // Licensed under the MIT License.
  3. using System;
  4. using System.Diagnostics.ContractsLight;
  5. using System.Runtime.InteropServices;
  6. using BuildXL.Utilities;
  7. namespace BuildXL.Native.Streams
  8. {
  9. /// <summary>
  10. /// Buffer for operatins on an <see cref="AsyncFileStream" />. This class manages the contents and pinning of a backing byte buffer.
  11. /// Since this buffer is targetted at async operation in which buffer filling / flushing (actual I/O requests) are fully separate from
  12. /// stream reads and writes, it tracks a <see cref="BufferState" /> (the buffer may be locked during a background operation to fill or flush it;
  13. /// it is either a read buffer or a write buffer if not locked) and a <see cref="BufferLockReason" /> (fill or flush, when locked).
  14. ///
  15. /// This class is not thread safe.
  16. /// </summary>
  17. public sealed class FileBuffer : IDisposable
  18. {
  19. /// <summary>
  20. /// Overall state. This state indicates if the buffer is presently usable for reading or writing.
  21. /// </summary>
  22. public enum BufferState
  23. {
  24. /// <summary>
  25. /// Empty buffer.
  26. /// </summary>
  27. Empty,
  28. /// <summary>
  29. /// Buffer contains readable bytes.
  30. /// </summary>
  31. Read,
  32. /// <summary>
  33. /// Buffer contains written bytes.
  34. /// </summary>
  35. Write,
  36. /// <summary>
  37. /// Buffer is locked, and not readable or writable until an associated flush or fill operation completes.
  38. /// </summary>
  39. Locked,
  40. }
  41. /// <summary>
  42. /// Completion status of a read or write operation.
  43. /// </summary>
  44. public enum BufferOperationStatus
  45. {
  46. /// <summary>
  47. /// Operation completed, without filling or exhausting the buffer.
  48. /// </summary>
  49. CapacityRemaining,
  50. /// <summary>
  51. /// Buffer is empty following a read; refill required to get more bytes.
  52. /// </summary>
  53. ReadExhausted,
  54. /// <summary>
  55. /// Write buffer is full, or a read cannot proceed yet (flush required to become a read buffer).
  56. /// </summary>
  57. FlushRequired,
  58. }
  59. /// <summary>
  60. /// Reason for being in the <see cref="BufferState.Locked"/> state.
  61. /// </summary>
  62. private enum BufferLockReason
  63. {
  64. /// <summary>
  65. /// Not locked.
  66. /// </summary>
  67. None,
  68. /// <summary>
  69. /// Fill (background read from disk)
  70. /// </summary>
  71. Fill,
  72. /// <summary>
  73. /// Flush (background write to disk)
  74. /// </summary>
  75. Flush,
  76. }
  77. private GCHandle m_lockedStateGCHandle;
  78. private byte[] Buffer => m_pooledBufferWrapper.Instance;
  79. private readonly PooledObjectWrapper<byte[]> m_pooledBufferWrapper;
  80. private readonly int m_bufferSize;
  81. private BufferState m_currentBufferState;
  82. private BufferLockReason m_lockReason;
  83. private int m_bufferFilledSize;
  84. private int m_bufferPosition;
  85. /// <nodoc />
  86. public FileBuffer(int bufferSize)
  87. {
  88. Contract.Requires(bufferSize > 0);
  89. m_pooledBufferWrapper = Pools.ByteArrayPool.GetInstance(bufferSize);
  90. m_bufferSize = bufferSize;
  91. }
  92. /// <summary>
  93. /// Current state. See <see cref="BufferState"/>
  94. /// </summary>
  95. public BufferState State
  96. {
  97. get { return m_currentBufferState; }
  98. }
  99. /// <summary>
  100. /// Buffer size
  101. /// </summary>
  102. public int Capacity
  103. {
  104. get { return m_bufferSize; }
  105. }
  106. /// <summary>
  107. /// Attempts to read bytes. This may require a flush (if presently a write buffer) or a fill
  108. /// (if no readable bytes are available), as indicated by the returned status.
  109. /// </summary>
  110. public BufferOperationStatus Read(byte[] buffer, int offset, int count, out int bytesRead)
  111. {
  112. Contract.Requires(State != BufferState.Locked);
  113. if (count == 0)
  114. {
  115. bytesRead = 0;
  116. return BufferOperationStatus.CapacityRemaining;
  117. }
  118. if (m_currentBufferState == BufferState.Write)
  119. {
  120. bytesRead = 0;
  121. return BufferOperationStatus.FlushRequired;
  122. }
  123. if (m_currentBufferState == BufferState.Empty)
  124. {
  125. bytesRead = 0;
  126. return BufferOperationStatus.ReadExhausted;
  127. }
  128. Contract.Assert(m_currentBufferState == BufferState.Read);
  129. int maxReadable = m_bufferFilledSize - m_bufferPosition;
  130. int bytesToCopy = Math.Min(count, maxReadable);
  131. Contract.Assume(bytesToCopy > 0, "BufferType should have been empty.");
  132. System.Buffer.BlockCopy(Buffer, m_bufferPosition, buffer, offset, bytesToCopy);
  133. m_bufferPosition += bytesToCopy;
  134. Contract.Assert(m_bufferPosition <= m_bufferFilledSize);
  135. bytesRead = bytesToCopy;
  136. if (m_bufferPosition == m_bufferFilledSize)
  137. {
  138. Discard();
  139. return BufferOperationStatus.ReadExhausted;
  140. }
  141. else
  142. {
  143. return BufferOperationStatus.CapacityRemaining;
  144. }
  145. }
  146. /// <summary>
  147. /// Attempts to write bytes. This may require a flush (if presently full and a write buffer).
  148. /// If presently a read buffer, the read buffer contents are silently discarded.
  149. /// </summary>
  150. public BufferOperationStatus Write(byte[] buffer, int offset, int count, out int bytesWritten)
  151. {
  152. Contract.Requires(State != BufferState.Locked);
  153. if (count == 0)
  154. {
  155. bytesWritten = 0;
  156. return BufferOperationStatus.CapacityRemaining;
  157. }
  158. if (m_currentBufferState == BufferState.Read)
  159. {
  160. // TODO: Counter?
  161. Discard();
  162. }
  163. Contract.Assert(m_currentBufferState == BufferState.Write || m_currentBufferState == BufferState.Empty);
  164. m_currentBufferState = BufferState.Write;
  165. if (m_bufferPosition == m_bufferSize)
  166. {
  167. bytesWritten = 0;
  168. return BufferOperationStatus.FlushRequired;
  169. }
  170. int maxWritable = m_bufferSize - m_bufferPosition;
  171. Contract.Assert(maxWritable > 0);
  172. int bytesToCopy = Math.Min(count, maxWritable);
  173. System.Buffer.BlockCopy(buffer, offset, Buffer, m_bufferPosition, bytesToCopy);
  174. m_bufferPosition += bytesToCopy;
  175. Contract.Assert(m_bufferPosition <= m_bufferSize);
  176. bytesWritten = bytesToCopy;
  177. return (m_bufferPosition == m_bufferSize) ? BufferOperationStatus.FlushRequired : BufferOperationStatus.CapacityRemaining;
  178. }
  179. private void Lock(BufferLockReason lockReason)
  180. {
  181. Contract.Requires(lockReason != BufferLockReason.None);
  182. Contract.Requires(State == BufferState.Empty || State == BufferState.Write);
  183. m_lockReason = lockReason;
  184. m_currentBufferState = BufferState.Locked;
  185. Contract.Assume(!m_lockedStateGCHandle.IsAllocated);
  186. m_lockedStateGCHandle = GCHandle.Alloc(Buffer, GCHandleType.Pinned);
  187. }
  188. private void Unlock(BufferState newState)
  189. {
  190. Contract.Requires(State == BufferState.Locked);
  191. Contract.Requires(newState != BufferState.Locked);
  192. m_lockReason = BufferLockReason.None;
  193. m_currentBufferState = newState;
  194. m_lockedStateGCHandle.Free();
  195. }
  196. /// <summary>
  197. /// Transitions the buffer to the <see cref="BufferState.Locked"/> state, and returns a buffer pointer
  198. /// for a native fill operation. <see cref="FinishFillAndUnlock"/> should be called to complete this.
  199. /// </summary>
  200. public unsafe void LockForFill(out byte* pinnedBuffer, out int pinnedBufferLength)
  201. {
  202. Contract.Requires(State == BufferState.Empty);
  203. Lock(BufferLockReason.Fill);
  204. pinnedBuffer = (byte*)m_lockedStateGCHandle.AddrOfPinnedObject();
  205. pinnedBufferLength = m_bufferSize;
  206. }
  207. /// <summary>
  208. /// Transitions the buffer to the <see cref="BufferState.Locked"/> state, and returns a buffer pointer
  209. /// for a native fill operation. <see cref="FinishFlushAndUnlock"/> should be called to complete this
  210. /// and enter <see cref="BufferState.Read"/>.
  211. /// </summary>
  212. public unsafe void LockForFlush(out byte* pinnedBuffer, out int bytesToFlush)
  213. {
  214. Contract.Requires(State == BufferState.Write);
  215. Lock(BufferLockReason.Flush);
  216. pinnedBuffer = (byte*)m_lockedStateGCHandle.AddrOfPinnedObject();
  217. bytesToFlush = m_bufferPosition;
  218. }
  219. /// <summary>
  220. /// Finishes a flush operation. This transitions the buffer out of the <see cref="BufferState.Locked"/> state.
  221. /// </summary>
  222. public void FinishFlushAndUnlock(int numberOfBytesFlushed)
  223. {
  224. Contract.Requires(State == BufferState.Locked);
  225. Contract.Requires(numberOfBytesFlushed >= 0);
  226. Contract.Assume(m_lockReason == BufferLockReason.Flush);
  227. bool entirelyFlushed = numberOfBytesFlushed == m_bufferPosition;
  228. Contract.Assume(numberOfBytesFlushed <= m_bufferPosition, "Too many bytes flushed; number of bytes to flush is indicated by LockForFlush");
  229. if (entirelyFlushed)
  230. {
  231. m_bufferFilledSize = 0;
  232. m_bufferPosition = 0;
  233. }
  234. // TODO: Must handle partial flushes for writable AsyncFileStreams to work.
  235. Contract.Assume(entirelyFlushed);
  236. Unlock(entirelyFlushed ? BufferState.Empty : BufferState.Write);
  237. }
  238. /// <summary>
  239. /// Finishes a fill operation. This transitions the buffer out of the <see cref="BufferState.Locked"/> state.
  240. /// </summary>
  241. public void FinishFillAndUnlock(int numberOfBytesFilled)
  242. {
  243. Contract.Requires(State == BufferState.Locked);
  244. Contract.Requires(numberOfBytesFilled >= 0 && numberOfBytesFilled <= Capacity);
  245. Contract.Assume(m_lockReason == BufferLockReason.Fill);
  246. m_bufferFilledSize = numberOfBytesFilled;
  247. m_bufferPosition = 0;
  248. Unlock(numberOfBytesFilled == 0 ? BufferState.Empty : BufferState.Read);
  249. }
  250. /// <summary>
  251. /// Discards the current buffer, if doing so does not cause data loss (i.e., there are not un-flushed written bytes in the buffer).
  252. /// </summary>
  253. public void Discard()
  254. {
  255. Contract.Requires(State == BufferState.Read || State == BufferState.Empty);
  256. m_bufferFilledSize = 0;
  257. m_bufferPosition = 0;
  258. m_currentBufferState = BufferState.Empty;
  259. }
  260. /// <summary>
  261. /// Dispose the current FileBuffer. All pooled resources should be returned.
  262. /// </summary>
  263. public void Dispose()
  264. {
  265. m_pooledBufferWrapper.Dispose();
  266. }
  267. }
  268. }