File: Workspace\Solution\VersionStamp.cs
Web Access
Project: src\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;
        }
    }
}