| File: Workspace\Solution\VersionStamp.cs | Web Access |
| Project: src\roslyn\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Diagnostics; using System.Threading; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis; /// <summary> /// VersionStamp should be only used to compare versions returned by same API. /// </summary> public readonly struct VersionStamp : IEquatable<VersionStamp> { public static VersionStamp Default => default; private const int GlobalVersionMarker = -1; private const int InitialGlobalVersion = 10000; /// <summary> /// global counter to avoid collision within same session. /// it starts with a big initial number just for a clarity in debugging /// </summary> private static int s_globalVersion = InitialGlobalVersion; /// <summary> /// time stamp /// </summary> private readonly DateTime _utcLastModified; /// <summary> /// indicate whether there was a collision on same item /// </summary> private readonly int _localIncrement; /// <summary> /// unique version in same session /// </summary> private readonly int _globalIncrement; private VersionStamp(DateTime utcLastModified) : this(utcLastModified, 0) { } private VersionStamp(DateTime utcLastModified, int localIncrement) : this(utcLastModified, localIncrement, GetNextGlobalVersion()) { } private VersionStamp(DateTime utcLastModified, int localIncrement, int globalIncrement) { if (utcLastModified != default && utcLastModified.Kind != DateTimeKind.Utc) { throw new ArgumentException(WorkspacesResources.DateTimeKind_must_be_Utc, nameof(utcLastModified)); } _utcLastModified = utcLastModified; _localIncrement = localIncrement; _globalIncrement = globalIncrement; } /// <summary> /// Creates a new instance of a VersionStamp. /// </summary> public static VersionStamp Create() => new(DateTime.UtcNow); /// <summary> /// Creates a new instance of a version stamp based on the specified DateTime. /// </summary> public static VersionStamp Create(DateTime utcTimeLastModified) => new(utcTimeLastModified); /// <summary> /// compare two different versions and return either one of the versions if there is no collision, otherwise, create a new version /// that can be used later to compare versions between different items /// </summary> public VersionStamp GetNewerVersion(VersionStamp version) { // * NOTE * // in current design/implementation, there are 4 possible ways for a version to be created. // // 1. created from a file stamp (most likely by starting a new session). "increment" will have 0 as value // 2. created by modifying existing item (text changes, project changes etc). // "increment" will have either 0 or previous increment + 1 if there was a collision. // 3. created from deserialization (probably by using persistent service). // 4. created by accumulating versions of multiple items. // // and this method is the one that is responsible for #4 case. if (_utcLastModified > version._utcLastModified) { return this; } if (_utcLastModified == version._utcLastModified) { var thisGlobalVersion = GetGlobalVersion(this); var thatGlobalVersion = GetGlobalVersion(version); if (thisGlobalVersion == thatGlobalVersion) { // given versions are same one return this; } // mark it as global version // global version can't be moved to newer version. return new VersionStamp(_utcLastModified, (thisGlobalVersion > thatGlobalVersion) ? thisGlobalVersion : thatGlobalVersion, GlobalVersionMarker); } return version; } /// <summary> /// Gets a new VersionStamp that is guaranteed to be newer than its base one /// this should only be used for same item to move it to newer version /// </summary> public VersionStamp GetNewerVersion() { // global version can't be moved to newer version Debug.Assert(_globalIncrement != GlobalVersionMarker); var now = DateTime.UtcNow; var incr = (now == _utcLastModified) ? _localIncrement + 1 : 0; return new VersionStamp(now, incr); } /// <summary> /// Returns the serialized text form of the VersionStamp. /// </summary> public override string ToString() { // 'o' is the roundtrip format that captures the most detail. return _utcLastModified.ToString("o") + "-" + _globalIncrement + "-" + _localIncrement; } public override int GetHashCode() => Hash.Combine(_utcLastModified.GetHashCode(), _localIncrement); public override bool Equals(object? obj) { if (obj is VersionStamp v) { return this.Equals(v); } return false; } public bool Equals(VersionStamp version) { if (_utcLastModified == version._utcLastModified) { return GetGlobalVersion(this) == GetGlobalVersion(version); } return false; } public static bool operator ==(VersionStamp left, VersionStamp right) => left.Equals(right); public static bool operator !=(VersionStamp left, VersionStamp right) => !left.Equals(right); private static int GetGlobalVersion(VersionStamp version) { // global increment < 0 means it is a global version which has its global increment in local increment return version._globalIncrement >= 0 ? version._globalIncrement : version._localIncrement; } private static int GetNextGlobalVersion() { // REVIEW: not sure what is best way to wrap it when it overflows. should I just throw or don't care. // with 50ms (typing) as an interval for a new version, it gives more than 1 year before int32 to overflow. // with 5ms as an interval, it gives more than 120 days before it overflows. // since global version is only for per VS session, I think we don't need to worry about overflow. // or we could use Int64 which will give more than a million years turn around even on 1ms interval. // this will let versions to be compared safely between multiple items // without worrying about collision within same session var globalVersion = Interlocked.Increment(ref VersionStamp.s_globalVersion); return globalVersion; } internal TestAccessor GetTestAccessor() => new(this); internal readonly struct TestAccessor(VersionStamp versionStamp) { /// <summary> /// True if this VersionStamp is newer than the specified one. /// </summary> internal bool IsNewerThan(in VersionStamp version) { if (versionStamp._utcLastModified > version._utcLastModified) { return true; } if (versionStamp._utcLastModified == version._utcLastModified) { return GetGlobalVersion(versionStamp) > GetGlobalVersion(version); } return false; } } }