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