Use comma separator for .kotlin-profile files and semicolon for .profile

Right now expected format for FUS files is different for old Fus files
and new ones.
Comma or semicolon is used to separate a list of metric's values.

#KT-79585: Varification pending
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FusStatisticsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FusStatisticsIT.kt
index 5469e3e..abb294c 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FusStatisticsIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FusStatisticsIT.kt
@@ -20,6 +20,7 @@
 import org.jetbrains.kotlin.gradle.util.filterKotlinFusFiles
 import org.jetbrains.kotlin.gradle.util.replaceText
 import org.jetbrains.kotlin.gradle.util.swiftExportEmbedAndSignEnvVariables
+import org.jetbrains.kotlin.konan.target.HostManager
 import org.jetbrains.kotlin.statistics.metrics.StringAnonymizationPolicy
 import org.junit.jupiter.api.DisplayName
 import org.junit.jupiter.api.condition.OS
@@ -197,6 +198,7 @@
                 build("linkDebugExecutableHost", "-Pkotlin.session.logger.root.path=$projectPath") {
                     assertOutputDoesNotContainFusErrors()
                     fusStatisticsDirectory.assertFusReportContains("KOTLIN_INCREMENTAL_NATIVE_ENABLED=true")
+                    fusStatisticsDirectory.assertFusReportContainsMetricWithValues("MPP_PLATFORMS", listOf("common", HostManager.host.name))
                 }
             }
         }
@@ -747,7 +749,7 @@
     @GradleTest
     @JvmGradlePluginTests
     fun testCompilerExecutionSettings(gradleVersion: GradleVersion) {
-        val kotlinVersion = StringAnonymizationPolicy.ComponentVersionAnonymizer().anonymize(KOTLIN_VERSION)
+        val kotlinVersion = StringAnonymizationPolicy.ComponentVersionAnonymizer().anonymize(KOTLIN_VERSION, ";")
         project("simpleProject", gradleVersion) {
             assertNoErrorFilesCreated {
                 build("compileKotlin", "-Pkotlin.session.logger.root.path=$projectPath") {
@@ -860,3 +862,8 @@
     assertOutputDoesNotContain("finish-profile already exists")
     assertOutputDoesNotContain("Unable to collect finish file for build")
 }
+
+private fun Path.assertFusReportContainsMetricWithValues(metricName: String, expectedValues: List<String>) {
+    assertFilesCombinedContains(filterKotlinFusFiles(), "$metricName=${expectedValues.joinToString(",")}")
+    assertFilesCombinedContains(filterBackwardCompatibilityKotlinFusFiles(), "$metricName=${expectedValues.joinToString(";")}")
+}
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildFinishBuildService.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildFinishBuildService.kt
index 573b9c8..95407e2 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildFinishBuildService.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildFinishBuildService.kt
@@ -25,11 +25,13 @@
 import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
 import org.jetbrains.kotlin.gradle.utils.kotlinErrorsDir
 import org.jetbrains.kotlin.statistics.fileloggers.MetricsContainer
+import org.jetbrains.kotlin.statistics.fileloggers.MetricsContainer.Companion.addMetricFromFusKotlinProfileFile
+import org.jetbrains.kotlin.statistics.fileloggers.MetricsContainer.Companion.createValidateAndAnonymizeCopy
 import java.io.File
 import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.String
 
-internal abstract class BuildFinishBuildService : BuildService<BuildFinishBuildService.Parameters>, AutoCloseable, OperationCompletionListener {
+internal abstract class BuildFinishBuildService : BuildService<BuildFinishBuildService.Parameters>, AutoCloseable,
+    OperationCompletionListener {
     protected val buildId = parameters.buildId.get()
     private val log = Logging.getLogger(this.javaClass)
     private val errorWasReported = AtomicBoolean(false)
@@ -50,7 +52,11 @@
         private val serviceName =
             "${BuildFinishBuildService::class.java.canonicalName}_${BuildFinishBuildService::class.java.classLoader.hashCode()}"
 
-        fun registerIfAbsent(project: Project, buildUidService: Provider<BuildUidService>, kotlinPluginVersion: String): Provider<BuildFinishBuildService>? {
+        fun registerIfAbsent(
+            project: Project,
+            buildUidService: Provider<BuildUidService>,
+            kotlinPluginVersion: String,
+        ): Provider<BuildFinishBuildService>? {
             if (!project.buildServiceShouldBeCreated) {
                 return null
             }
@@ -88,21 +94,19 @@
             log: Logger,
         ): Errors {
             try {
-                val metricContainer = MetricsContainer()
+                val metricContainer = MetricsContainer.createMetricsContainerForProfileFile()
 
                 fusReportDirectory.listFiles()
                     .filter { it.name.startsWith(buildUid) && (it.name.endsWith("plugin-profile") || it.name.endsWith("kotlin-profile")) }
                     .forEach {
-                        MetricsContainer.readFromFile(it) {
-                            metricContainer.populateFromMetricsContainer(it)
-                        }
+                        metricContainer.addMetricFromFusKotlinProfileFile(it)
                     }
 
                 val fusFile = fusReportDirectory.resolve("$buildUid.profile")
                 fusFile.writer().buffered().use {
                     it.appendLine("Build: $buildUid")
                     it.appendLine("Kotlin version: $kotlinVersion")
-                    metricContainer.flush(it)
+                    metricContainer.createValidateAndAnonymizeCopy().flush(it)
                 }
 
                 if (!fusReportDirectory.resolve("$buildUid.finish-profile").createNewFile()) {
@@ -145,4 +149,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/NonSynchronizedMetricsContainer.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/NonSynchronizedMetricsContainer.kt
index 6b56894..1a5b3c3 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/NonSynchronizedMetricsContainer.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/NonSynchronizedMetricsContainer.kt
@@ -1,13 +1,15 @@
 package org.jetbrains.kotlin.gradle.plugin.statistics
 
-import org.jetbrains.kotlin.statistics.metrics.*
+import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics
+import org.jetbrains.kotlin.statistics.metrics.NumericalMetrics
+import org.jetbrains.kotlin.statistics.metrics.StatisticsValuesConsumer
+import org.jetbrains.kotlin.statistics.metrics.StringMetrics
 import java.io.Serializable
 
 /*
  * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
  * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
  */
-
 internal open class NonSynchronizedMetricsContainer : StatisticsValuesConsumer, Serializable {
     data class MetricDescriptor<T : Comparable<T>>(val name: T, val subprojectName: String?) : Comparable<MetricDescriptor<T>> {
         override fun compareTo(other: MetricDescriptor<T>): Int {
@@ -20,52 +22,57 @@
         }
     }
 
-    private val numericalMetrics = HashMap<MetricDescriptor<NumericalMetrics>, IMetricContainer<Long>>()
+    private val numericalMetrics = HashMap<MetricDescriptor<NumericalMetrics>, MutableList<Pair<Long, Long?>>>()
 
-    private val booleanMetrics = HashMap<MetricDescriptor<BooleanMetrics>, IMetricContainer<Boolean>>()
+    private val booleanMetrics = HashMap<MetricDescriptor<BooleanMetrics>, MutableList<Pair<Boolean, Long?>>>()
 
-    private val stringMetrics = HashMap<MetricDescriptor<StringMetrics>, IMetricContainer<String>>()
+    private val stringMetrics = HashMap<MetricDescriptor<StringMetrics>, MutableList<Pair<String, Long?>>>()
 
-    private fun <V, T : IMetricContainerFactory<V>, K : MetricDescriptor<*>> putToCollection(
-        collection: MutableMap<K, IMetricContainer<V>>,
+    private fun <V, K : MetricDescriptor<*>> putToCollection(
+        collection: MutableMap<K, MutableList<Pair<V, Long?>>>,
         key: K,
-        type: T,
         value: V,
         weight: Long? = null,
     ) {
-        collection.getOrPut(key) { type.newMetricContainer() }.addValue(value, weight)
+        collection.getOrPut(key) { ArrayList<Pair<V, Long?>>() }.add(value to weight)
     }
 
     override fun report(metric: BooleanMetrics, value: Boolean, subprojectName: String?, weight: Long?): Boolean {
-        putToCollection(booleanMetrics, MetricDescriptor(metric, subprojectName), metric.type, value, weight)
+        putToCollection(booleanMetrics, MetricDescriptor(metric, subprojectName), value, weight)
         return true
     }
 
     override fun report(metric: NumericalMetrics, value: Long, subprojectName: String?, weight: Long?): Boolean {
-        putToCollection(numericalMetrics, MetricDescriptor(metric, subprojectName), metric.type, value, weight)
+        putToCollection(numericalMetrics, MetricDescriptor(metric, subprojectName), value, weight)
         return true
     }
 
     override fun report(metric: StringMetrics, value: String, subprojectName: String?, weight: Long?): Boolean {
-        putToCollection(stringMetrics, MetricDescriptor(metric, subprojectName), metric.type, value, weight)
+        putToCollection(stringMetrics, MetricDescriptor(metric, subprojectName), value, weight)
         return true
     }
 
     open fun readFromMetricConsumer(metricConsumer: NonSynchronizedMetricsContainer) {
         metricConsumer.booleanMetrics.forEach {
-            putToCollection(booleanMetrics, MetricDescriptor(it.key.name, it.key.subprojectName), it.key.name.type, it.value.getValue()!!)
+            it.value.forEach { (value, weight) -> putToCollection(booleanMetrics, MetricDescriptor(it.key.name, it.key.subprojectName), value, weight) }
         }
         metricConsumer.stringMetrics.forEach {
-            putToCollection(stringMetrics, MetricDescriptor(it.key.name, it.key.subprojectName), it.key.name.type, it.value.getValue()!!)
+            it.value.forEach { (value, weight) -> putToCollection(stringMetrics, MetricDescriptor(it.key.name, it.key.subprojectName), value, weight) }
         }
         metricConsumer.numericalMetrics.forEach {
-            putToCollection(numericalMetrics, MetricDescriptor(it.key.name, it.key.subprojectName), it.key.name.type, it.value.getValue()!!)
+            it.value.forEach { (value, weight) -> putToCollection(numericalMetrics, MetricDescriptor(it.key.name, it.key.subprojectName), value, weight) }
         }
     }
 
     open fun sendToConsumer(metricConsumer: StatisticsValuesConsumer) {
-        booleanMetrics.forEach { metricConsumer.report(it.key.name, it.value.getValue()!!, it.key.subprojectName) }
-        numericalMetrics.forEach { metricConsumer.report(it.key.name, it.value.getValue()!!, it.key.subprojectName) }
-        stringMetrics.forEach { metricConsumer.report(it.key.name, it.value.getValue()!!, it.key.subprojectName) }
+        booleanMetrics.forEach {
+            it.value.forEach { (value, weight) -> metricConsumer.report(it.key.name, value, it.key.subprojectName, weight) }
+        }
+        numericalMetrics.forEach {
+            it.value.forEach { (value, weight) -> metricConsumer.report(it.key.name, value, it.key.subprojectName, weight) }
+        }
+        stringMetrics.forEach {
+            it.value.forEach { (value, weight) -> metricConsumer.report(it.key.name, value, it.key.subprojectName, weight) }
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildFinishBuildServiceTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildFinishBuildServiceTest.kt
index f096549..4b24d1e 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildFinishBuildServiceTest.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildFinishBuildServiceTest.kt
@@ -9,9 +9,11 @@
 import org.jetbrains.kotlin.gradle.plugin.statistics.BuildFinishBuildService
 import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics
 import org.jetbrains.kotlin.statistics.metrics.NumericalMetrics
+import org.jetbrains.kotlin.statistics.metrics.StringMetrics
 import org.junit.jupiter.api.io.TempDir
 import java.io.File
 import kotlin.test.Test
+import kotlin.test.assertContains
 import kotlin.test.assertTrue
 
 class BuildFinishBuildServiceTest {
@@ -31,6 +33,11 @@
                 unknown-metric=1
 
                 ${NumericalMetrics.COMPILATION_DURATION}=10
+                ${StringMetrics.OS_VERSION}=1.0.0-SNAPSHOT
+                ${StringMetrics.IDES_INSTALLED}=invalid_version,AS,WC
+                ${BooleanMetrics.ENABLED_COMPILER_REFERENCE_INDEX}=true
+                ${BooleanMetrics.KOTLIN_PROGRESSIVE_MODE}=true
+ 
                 BUILD FINISHED
             """.trimIndent()
         )
@@ -39,6 +46,9 @@
                 unknown-metric=1
                 wrong format
                 ${NumericalMetrics.COMPILATION_DURATION}=10
+                ${StringMetrics.OS_VERSION}=invalid_version
+                ${BooleanMetrics.ENABLED_COMPILER_REFERENCE_INDEX}=invalid
+                ${BooleanMetrics.KOTLIN_PROGRESSIVE_MODE}=invalid
                 BUILD FINISHED
             """.trimIndent()
         )
@@ -51,6 +61,10 @@
         fusDir.resolve("$buildId.kajfsjfh.kotlin-profile").writeText(
             """
                 ${BooleanMetrics.BUILD_SCAN_BUILD_REPORT}=true
+                ${StringMetrics.OS_VERSION}=2.0.0-SNAPSHOT
+                ${StringMetrics.IDES_INSTALLED}=IU
+                ${BooleanMetrics.ENABLED_COMPILER_REFERENCE_INDEX}=false
+                ${BooleanMetrics.KOTLIN_PROGRESSIVE_MODE}=false
                 BUILD FINISHED
             """.trimIndent()
         )
@@ -72,6 +86,10 @@
         assertTrue("Profile file should contain valid metrics") {
             profileContent.contains("${NumericalMetrics.COMPILATION_DURATION}=20")
         }
+        assertContains(profileContent, "${StringMetrics.OS_VERSION}=2.0.0-snapshot")
+        assertContains(profileContent, "${StringMetrics.IDES_INSTALLED}=AS;IU;WC;UNEXPECTED-VALUE")
+        assertContains(profileContent,"${BooleanMetrics.ENABLED_COMPILER_REFERENCE_INDEX}=true")
+        assertContains(profileContent,"${BooleanMetrics.KOTLIN_PROGRESSIVE_MODE}=false")
         assertTrue("Profile file should not contain metrics from another build") {
             !profileContent.contains(BooleanMetrics.TESTS_EXECUTED.name)
         }
diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildSessionLoggerTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildSessionLoggerTest.kt
index 1e50bc8..78b0964 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildSessionLoggerTest.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/statistics/BuildSessionLoggerTest.kt
@@ -8,9 +8,12 @@
 import org.jetbrains.kotlin.statistics.BuildSessionLogger
 import org.jetbrains.kotlin.statistics.BuildSessionLogger.Companion.FUS_KOTLIN_FILE_NAME_SUFFIX
 import org.jetbrains.kotlin.statistics.fileloggers.MetricsContainer
+import org.jetbrains.kotlin.statistics.fileloggers.MetricsContainer.Companion.addMetricFromFusKotlinProfileFile
 import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics
 import org.jetbrains.kotlin.statistics.metrics.NumericalMetrics
+import org.jetbrains.kotlin.statistics.metrics.StringAnonymizationPolicy
 import org.jetbrains.kotlin.statistics.metrics.StringMetrics
+import org.jetbrains.kotlin.statistics.metrics.StringOverridePolicy
 import java.io.File
 import java.nio.file.Files
 import java.util.*
@@ -169,29 +172,72 @@
     fun testSaveAndReadAllMetrics() {
         val logger = BuildSessionLogger(statsFolder)
         logger.startBuildSession("test")
-        for (metric in StringMetrics.values()) {
-            logger.report(metric, "value")
-            logger.report(metric, metric.name)
+        for (metric in StringMetrics.entries) {
+            when (val anonymization = metric.anonymization) {
+                is StringAnonymizationPolicy.ComponentVersionAnonymizer -> {
+                    logger.report(metric, "1.2.3")
+                    logger.report(metric, "1.2.3-SNAPSHOT")
+                }
+                is StringAnonymizationPolicy.AllowedListAnonymizer -> {
+                    anonymization.allowedValues.sorted().forEach {
+                        logger.report(metric, it)
+                    }
+                }
+                is StringAnonymizationPolicy.RegexControlled -> logger.report(metric, metric.name)
+                else -> {
+                    logger.report(metric, "value")
+                    logger.report(metric, metric.name)
+                }
+            }
         }
-        for (metric in BooleanMetrics.values()) {
+        for (metric in BooleanMetrics.entries) {
             logger.report(metric, true)
         }
-        for (metric in NumericalMetrics.values()) {
+        for (metric in NumericalMetrics.entries) {
             logger.report(metric, System.currentTimeMillis())
         }
-        logger.finishBuildSession()
 
-        MetricsContainer.readFromFile(statsFolder.listFiles()?.single() ?: fail("Could not find stat file")) {
-            for (metric in StringMetrics.values()) {
-                assertNotNull(it.getMetric(metric), "Could not find metric ${metric.name}")
-            }
-            for (metric in BooleanMetrics.values()) {
-                assertTrue(it.getMetric(metric)?.getValue() != null, "Could not find metric ${metric.name}")
-            }
-            for (metric in NumericalMetrics.values()) {
-                assertNotNull(it.getMetric(metric), "Could not find metric ${metric.name}")
+        logger.finishBuildSession() // create kotlin-profile fus file with comma-separated values
+
+        // read metrics from kotlin-profile file in old format with semicolon-separated values
+        val metricContainer = MetricsContainer.createMetricsContainerForProfileFile()
+
+        metricContainer.addMetricFromFusKotlinProfileFile(statsFolder.listFiles()?.single() ?: fail("Could not find stat file"))
+
+        for (metric in StringMetrics.entries) {
+            val metricValue = metricContainer.getMetric(metric)?.getValue() ?: fail("Could not find metric ${metric.name}")
+
+            when (val anonymization = metric.anonymization) {
+                is StringAnonymizationPolicy.ComponentVersionAnonymizer -> validateMetricValueBasedOnOverrideRule(metric, listOf("1.2.3", "1.2.3-snapshot"), metricValue)
+                is StringAnonymizationPolicy.AllowedListAnonymizer -> validateMetricValueBasedOnOverrideRule(metric, anonymization.allowedValues.sorted(), metricValue)
+                is StringAnonymizationPolicy.RegexControlled -> assertMetricValueEquals(metric, metric.name, metricValue)
+                else -> validateMetricValueBasedOnOverrideRule(metric, listOf("value", metric.name), metricValue)
             }
         }
+
+        for (metric in BooleanMetrics.entries) {
+            assertTrue(metricContainer.getMetric(metric)?.getValue() != null, "Could not find metric ${metric.name}")
+        }
+
+        for (metric in NumericalMetrics.entries) {
+            assertNotNull(metricContainer.getMetric(metric), "Could not find metric ${metric.name}")
+        }
+    }
+
+    private fun validateMetricValueBasedOnOverrideRule(metric: StringMetrics, possibleExpectedValues: List<String>, actualValue: String) {
+        when (metric.type) {
+            StringOverridePolicy.OVERRIDE -> assertMetricValueEquals(metric, possibleExpectedValues.last(), actualValue)
+            StringOverridePolicy.OVERRIDE_VERSION_IF_NOT_SET -> assertMetricValueEquals(metric, possibleExpectedValues.first(), actualValue)
+            StringOverridePolicy.CONCAT -> assertMetricValueEquals(metric, possibleExpectedValues.joinToString(";"), actualValue)
+        }
+    }
+
+    private fun assertMetricValueEquals(metric: StringMetrics, expectedValue: String, actualValue: String) {
+        assertEquals(
+            expectedValue,
+            actualValue,
+            "Metric ${metric.name} contains unexpected value: expected $expectedValue, but found $actualValue"
+        )
     }
 
     @Test
diff --git a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/AnonymizerUtils.kt b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/AnonymizerUtils.kt
index 3ad68bf..b91b2f5 100644
--- a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/AnonymizerUtils.kt
+++ b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/AnonymizerUtils.kt
@@ -7,9 +7,11 @@
 
 import java.security.MessageDigest
 
+internal const val DEFAULT_SEPARATOR = ";"
+
 internal interface ValueAnonymizer<T> {
 
-    fun anonymize(t: T): T
+    fun anonymize(t: T, separator: String = DEFAULT_SEPARATOR): T
 
     fun anonymizeOnIdeSize(): Boolean = false
 
diff --git a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/BuildSessionLogger.kt b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/BuildSessionLogger.kt
index d5aaa38..280fdbd 100644
--- a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/BuildSessionLogger.kt
+++ b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/BuildSessionLogger.kt
@@ -46,7 +46,8 @@
 
     private var buildSession: BuildSession? = null
 
-    private val metricsContainer = MetricsContainer(forceValuesValidation)
+    //New FUS file should use comma (,) as a separator, but for old FUS files a semicolon (;) is used
+    private val metricsContainer = MetricsContainer.createMetricsContainerForKotlinProfileFile(forceValuesValidation)
 
     @Synchronized
     fun startBuildSession(buildUid: String) {
diff --git a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/fileloggers/MetricsContainer.kt b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/fileloggers/MetricsContainer.kt
index e4b7513..39ee9b2 100644
--- a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/fileloggers/MetricsContainer.kt
+++ b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/fileloggers/MetricsContainer.kt
@@ -16,7 +16,15 @@
 import java.nio.file.StandardOpenOption
 import java.util.*
 
-class MetricsContainer(private val forceValuesValidation: Boolean = false) : StatisticsValuesConsumer {
+/**
+ * A container class for managing and storing metrics of various types (numerical, boolean, and string)
+ * in a structured way. This class facilitates the collection, aggregation, and processing of
+ * metric data, supporting operations like reporting and reading metrics from files.
+ */
+class MetricsContainer(
+    private val forceValuesValidation: Boolean = false,
+    private val metricValuesSeparator: String = FUS_METRIC_SEPARATOR_FOR_BACKWARD_COMPATIBILITY_PROFILE_FILE,
+) : StatisticsValuesConsumer {
     data class MetricDescriptor(val name: String, val projectHash: String?) : Comparable<MetricDescriptor> {
         override fun compareTo(other: MetricDescriptor): Int {
             val compareNames = name.compareTo(other.name)
@@ -38,6 +46,10 @@
 
     companion object {
         private const val BUILD_SESSION_SEPARATOR = "BUILD FINISHED"
+        private const val METRIC_SEPARATOR = '='
+        private const val PROJECT_METRIC_NAME_SEPARATOR = '.'
+        private const val FUS_METRIC_SEPARATOR_FOR_KOTLIN_PROFILE_FILE = ","
+        private const val FUS_METRIC_SEPARATOR_FOR_BACKWARD_COMPATIBILITY_PROFILE_FILE = ";"
 
         val ENCODING = Charsets.UTF_8
 
@@ -47,11 +59,74 @@
 
         private val numericalMetricsMap = NumericalMetrics.values().associateBy(NumericalMetrics::name)
 
+        fun createMetricsContainerForProfileFile(forceValuesValidation: Boolean = false) =
+            MetricsContainer(
+                forceValuesValidation = forceValuesValidation,
+                metricValuesSeparator = FUS_METRIC_SEPARATOR_FOR_BACKWARD_COMPATIBILITY_PROFILE_FILE
+            )
+
+        fun createMetricsContainerForKotlinProfileFile(forceValuesValidation: Boolean = false) =
+            MetricsContainer(
+                forceValuesValidation = forceValuesValidation,
+                metricValuesSeparator = FUS_METRIC_SEPARATOR_FOR_KOTLIN_PROFILE_FILE
+            )
+
+        private fun MetricsContainer.addMetricToContainer(
+            metricDescriptor: MetricDescriptor,
+            representation: String,
+            separator: String = metricValuesSeparator,
+        ) {
+            stringMetricsMap[metricDescriptor.name]?.also { metricType ->
+                synchronized(metricsLock) {
+                    stringMetrics.getOrPut(metricDescriptor) {
+                        metricType.type.newMetricContainer()
+                    }.addValueFromStringPresentation(representation, separator)
+                }
+            }
+
+            booleanMetricsMap[metricDescriptor.name]?.also { metricType ->
+                synchronized(metricsLock) {
+                    booleanMetrics.getOrPut(metricDescriptor) {
+                        metricType.type.newMetricContainer()
+                    }.addValueFromStringPresentation(representation, separator)
+                }
+            }
+
+            numericalMetricsMap[metricDescriptor.name]?.also { metricType ->
+                synchronized(metricsLock) {
+                    numericalMetrics.getOrPut(metricDescriptor) {
+                        metricType.type.newMetricContainer()
+                    }.addValueFromStringPresentation(representation, separator)
+                }
+            }
+        }
+
+        // for new Fus files a comma (,) separator is used, but for old FUS files a semicolon (;) is used
+        private fun MetricsContainer.addMetricFromFusFile(file: File, separator: String) =
+            FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.WRITE, StandardOpenOption.READ).use { channel ->
+                BufferedReader(InputStreamReader(Channels.newInputStream(channel), ENCODING)).use {
+                    it.forEachLine { line ->
+                        if (line.contains(METRIC_SEPARATOR)) {
+                            // format: metricName.hash=string representation
+                            parseLine(line)?.also { (metricDescriptor, representation) ->
+                                addMetricToContainer(metricDescriptor, representation, separator)
+                            }
+                        }
+                    }
+                }
+            }
+
+        fun MetricsContainer.addMetricFromFusKotlinProfileFile(file: File) =
+            addMetricFromFusFile(file, separator = FUS_METRIC_SEPARATOR_FOR_KOTLIN_PROFILE_FILE)
+
+        // This method also used IntelliJ project in KotlinGradleFUSLoggerProcessor#process.
+        // It expects to read a fus file with a semicolon (;) separator for metric values.
         fun readFromFile(file: File, consumer: (MetricsContainer) -> Unit): Boolean {
             val channel = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.WRITE, StandardOpenOption.READ)
             channel.tryLock() ?: return false
 
             val inputStream = Channels.newInputStream(channel)
+
             try {
                 var container = MetricsContainer()
                 // Note: close is called at forEachLine
@@ -59,37 +134,10 @@
                     if (BUILD_SESSION_SEPARATOR == line) {
                         consumer.invoke(container)
                         container = MetricsContainer()
-                    } else if (line.contains('=')) {
+                    } else if (line.contains(METRIC_SEPARATOR)) {
                         // format: metricName.hash=string representation
-                        val lineParts = line.split('=')
-                        if (lineParts.size == 2) {
-                            val name = lineParts[0].split('.')[0]
-                            val subProjectHash = lineParts[0].split('.').getOrNull(1)
-                            val representation = lineParts[1]
-
-                            stringMetricsMap[name]?.also { metricType ->
-                                metricType.type.fromStringRepresentation(representation)?.also {
-                                    synchronized(container.metricsLock) {
-                                        container.stringMetrics[MetricDescriptor(name, subProjectHash)] = it
-                                    }
-                                }
-                            }
-
-                            booleanMetricsMap[name]?.also { metricType ->
-                                metricType.type.fromStringRepresentation(representation)?.also {
-                                    synchronized(container.metricsLock) {
-                                        container.booleanMetrics[MetricDescriptor(name, subProjectHash)] = it
-                                    }
-                                }
-                            }
-
-                            numericalMetricsMap[name]?.also { metricType ->
-                                metricType.type.fromStringRepresentation(representation)?.also {
-                                    synchronized(container.metricsLock) {
-                                        container.numericalMetrics[MetricDescriptor(name, subProjectHash)] = it
-                                    }
-                                }
-                            }
+                        parseLine(line)?.also { (metricDescriptor, representation) ->
+                            container.addMetricToContainer(metricDescriptor, representation)
                         }
                     }
                 }
@@ -98,8 +146,46 @@
             }
             return true
         }
+
+        private fun parseLine(line: String): Pair<MetricDescriptor, String>? {
+            val lineParts = line.split(METRIC_SEPARATOR)
+            if (lineParts.size == 2) {
+                val name = lineParts[0].split(PROJECT_METRIC_NAME_SEPARATOR)[0]
+                val subProjectHash = lineParts[0].split(PROJECT_METRIC_NAME_SEPARATOR).getOrNull(1)
+                val representation = lineParts[1]
+                return MetricDescriptor(name, subProjectHash) to representation
+            }
+            return null
+        }
+
+        fun MetricsContainer.createValidateAndAnonymizeCopy(): MetricsContainer {
+            val metricsContainer = MetricsContainer(forceValuesValidation, metricValuesSeparator)
+            numericalMetrics.forEach { (metricDescriptor, container) ->
+                val metric = numericalMetricsMap[metricDescriptor.name]
+                val value = container.getValue()
+                if (metric != null && value != null) {
+                    metricsContainer.report(metric, value, metricDescriptor.projectHash, null)
+                }
+            }
+            booleanMetrics.forEach { (metricDescriptor, container) ->
+                val metric = booleanMetricsMap[metricDescriptor.name]
+                val value = container.getValue()
+                if (metric != null && value != null) {
+                    metricsContainer.report(metric, value, metricDescriptor.projectHash, null)
+                }
+            }
+            stringMetrics.forEach { (metricDescriptor, container) ->
+                val metric = stringMetricsMap[metricDescriptor.name]
+                val value = container.getValue()
+                if (metric != null && value != null) {
+                    metricsContainer.report(metric, value, metricDescriptor.projectHash, null)
+                }
+            }
+            return metricsContainer
+        }
     }
 
+
     private fun processProjectName(subprojectName: String?, perProject: Boolean) =
         if (perProject && subprojectName != null) sha256(subprojectName) else null
 
@@ -109,9 +195,8 @@
     override fun report(metric: BooleanMetrics, value: Boolean, subprojectName: String?, weight: Long?): Boolean {
         val projectHash = getProjectHash(metric.perProject, subprojectName)
         synchronized(metricsLock) {
-            val metricContainer = booleanMetrics[MetricDescriptor(metric.name, projectHash)] ?: metric.type.newMetricContainer()
-                .also { booleanMetrics[MetricDescriptor(metric.name, projectHash)] = it }
-            metricContainer.addValue(metric.anonymization.anonymize(value), weight)
+            val metricContainer = booleanMetrics.getOrPut(MetricDescriptor(metric.name, projectHash)) { metric.type.newMetricContainer() }
+            metricContainer.addValue(metric.anonymization.anonymize(value, metricValuesSeparator), weight)
         }
         return true
     }
@@ -119,42 +204,34 @@
     override fun report(metric: NumericalMetrics, value: Long, subprojectName: String?, weight: Long?): Boolean {
         val projectHash = getProjectHash(metric.perProject, subprojectName)
         synchronized(metricsLock) {
-            val metricContainer = numericalMetrics[MetricDescriptor(metric.name, projectHash)] ?: metric.type.newMetricContainer()
-                .also { numericalMetrics[MetricDescriptor(metric.name, projectHash)] = it }
-            metricContainer.addValue(metric.anonymization.anonymize(value), weight)
+            val metricContainer = numericalMetrics.getOrPut(MetricDescriptor(metric.name, projectHash)) { metric.type.newMetricContainer() }
+            metricContainer.addValue(metric.anonymization.anonymize(value, metricValuesSeparator), weight)
         }
         return true
     }
 
+    internal fun validateMetric(metric: StringMetrics, value: String) {
+        if (value.contains(UNEXPECTED_VALUE) || !value.matches(Regex(metric.anonymization.validationRegexp(metricValuesSeparator)))) {
+            throw MetricValueValidationFailed("Metric ${metric.name} has value [${value}]. Validation regex: ${metric.anonymization.validationRegexp()}.")
+        }
+    }
+
+    internal fun anonymizeMetric(metric: StringMetrics, value: String) = metric.anonymization.anonymize(value, metricValuesSeparator)
+
     override fun report(metric: StringMetrics, value: String, subprojectName: String?, weight: Long?): Boolean {
         val projectHash = getProjectHash(metric.perProject, subprojectName)
         synchronized(metricsLock) {
-            val metricContainer = stringMetrics[MetricDescriptor(metric.name, projectHash)] ?: metric.type.newMetricContainer()
-                .also { stringMetrics[MetricDescriptor(metric.name, projectHash)] = it }
+            val metricContainer = stringMetrics.getOrPut(MetricDescriptor(metric.name, projectHash)) { metric.type.newMetricContainer() }
 
-            val anonymizedValue = metric.anonymization.anonymize(value)
+            val anonymizedValue = anonymizeMetric(metric, value)
             if (forceValuesValidation && !metric.anonymization.anonymizeOnIdeSize()) {
-                if (anonymizedValue.contains(UNEXPECTED_VALUE) || !anonymizedValue.matches(Regex(metric.anonymization.validationRegexp()))) {
-                    throw MetricValueValidationFailed("Metric ${metric.name} has value [${value}], after anonymization [${anonymizedValue}]. Validation regex: ${metric.anonymization.validationRegexp()}.")
-                }
+                validateMetric(metric, anonymizedValue)
             }
             metricContainer.addValue(anonymizedValue, weight)
         }
         return true
     }
 
-    fun populateFromMetricsContainer(metrics: MetricsContainer) = synchronized(metricsLock) {
-        metrics.booleanMetrics.forEach { (descriptor, metric) ->
-            report(BooleanMetrics.valueOf(descriptor.name), metric.getValue() ?: return@forEach, descriptor.projectHash, null)
-        }
-        metrics.stringMetrics.forEach { (descriptor, metric) ->
-            report(StringMetrics.valueOf(descriptor.name), metric.getValue() ?: return@forEach, descriptor.projectHash, null)
-        }
-        metrics.numericalMetrics.forEach { (descriptor, metric) ->
-            report(NumericalMetrics.valueOf(descriptor.name), metric.getValue() ?: return@forEach, descriptor.projectHash, null)
-        }
-    }
-
     fun flush(writer: BufferedWriter) {
         val allMetrics = TreeMap<MetricDescriptor, IMetricContainer<out Any>>()
         synchronized(metricsLock) {
@@ -165,7 +242,7 @@
         writer.appendLine()
         for (entry in allMetrics.entries) {
             val suffix = if (entry.key.projectHash == null) "" else ".${entry.key.projectHash}"
-            writer.appendLine("${entry.key.name}$suffix=${entry.value.toStringRepresentation()}")
+            writer.appendLine("${entry.key.name}$suffix$METRIC_SEPARATOR${entry.value.toStringRepresentation(metricValuesSeparator)}")
         }
 
         writer.appendLine(BUILD_SESSION_SEPARATOR)
@@ -185,6 +262,8 @@
         stringMetrics[MetricDescriptor(metric.name, null)]
     }
 
+    fun getStringMetricPresentation(metric: StringMetrics): String? = getMetric(metric)?.toStringRepresentation(metricValuesSeparator)
+
     fun getMetric(metric: BooleanMetrics): IMetricContainer<Boolean>? = synchronized(metricsLock) {
         booleanMetrics[MetricDescriptor(metric.name, null)]
     }
diff --git a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricContainers.kt b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricContainers.kt
index 4a4eb40..6051c8a 100644
--- a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricContainers.kt
+++ b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricContainers.kt
@@ -5,13 +5,16 @@
 
 package org.jetbrains.kotlin.statistics.metrics
 
+import org.jetbrains.kotlin.statistics.DEFAULT_SEPARATOR
 import java.io.Serializable
 import java.util.*
 
 interface IMetricContainer<T> : Serializable {
     fun addValue(t: T, weight: Long? = null)
 
-    fun toStringRepresentation(): String
+    fun addValueFromStringPresentation(str: String, separator: String)
+
+    fun toStringRepresentation(separator: String? = DEFAULT_SEPARATOR): String
 
     fun getValue(): T?
 }
@@ -19,27 +22,41 @@
 interface IMetricContainerFactory<T> {
     fun newMetricContainer(): IMetricContainer<T>
 
-    fun fromStringRepresentation(state: String): IMetricContainer<T>?
+    fun fromStringRepresentation(state: String, separator: String = DEFAULT_SEPARATOR): IMetricContainer<T>?
 }
 
-open class OverrideMetricContainer<T>() : IMetricContainer<T> {
+abstract class OverrideMetricContainer<T>() : IMetricContainer<T> {
     internal var myValue: T? = null
 
     override fun addValue(t: T, weight: Long?) {
         myValue = t
     }
 
-    internal constructor(v: T?) : this() {
-        myValue = v
-    }
-
-    override fun toStringRepresentation(): String {
+    override fun toStringRepresentation(separator: String?): String {
         return myValue?.toString() ?: "null"
     }
 
     override fun getValue() = myValue
 }
 
+open class OverrideStringMetricContainer() : OverrideMetricContainer<String>() {
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        addValue(str, null)
+    }
+}
+
+open class OverrideLongMetricContainer() : OverrideMetricContainer<Long>() {
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        str.toLongOrNull()?.also { addValue(it, null) }
+    }
+}
+
+open class OverrideBooleanMetricContainer(): OverrideMetricContainer<Boolean>() {
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        str.toBooleanStrictOrNull()?.also { addValue(it, null) }
+    }
+}
+
 class OverrideVersionMetricContainer() : OverrideMetricContainer<String>() {
     constructor(v: String) : this() {
         myValue = v
@@ -50,6 +67,10 @@
             myValue = t
         }
     }
+
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        addValue(str, null)
+    }
 }
 
 class SumMetricContainer() : OverrideMetricContainer<Long>() {
@@ -60,6 +81,10 @@
     override fun addValue(t: Long, weight: Long?) {
         myValue = (myValue ?: 0) + t
     }
+
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        str.toLongOrNull()?.also { addValue(it, null) }
+    }
 }
 
 class AverageMetricContainer() : IMetricContainer<Long> {
@@ -77,7 +102,11 @@
         totalWeight += w
     }
 
-    override fun toStringRepresentation(): String {
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        str.toLongOrNull()?.also { addValue(it, null) }
+    }
+
+    override fun toStringRepresentation(separator: String?): String {
         return getValue()?.toString() ?: "null"
     }
 
@@ -94,25 +123,29 @@
     override fun addValue(t: Boolean, weight: Long?) {
         myValue = (myValue ?: false) || t
     }
+
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        str.toBooleanStrictOrNull()?.also { addValue(it, null) }
+    }
 }
 
 class ConcatMetricContainer() : IMetricContainer<String> {
     private val myValues = TreeSet<String>()
 
-    companion object {
-        const val SEPARATOR = ";"
-    }
-
     constructor(values: Collection<String>) : this() {
         myValues.addAll(values)
     }
 
     override fun addValue(t: String, weight: Long?) {
-        myValues.add(t.replace(SEPARATOR, ","))
+        myValues.add(t)
     }
 
-    override fun toStringRepresentation(): String {
-        return myValues.sorted().joinToString(SEPARATOR)
+    override fun addValueFromStringPresentation(str: String, separator: String) {
+        str.split(separator).forEach { addValue(it, null) }
+    }
+
+    override fun toStringRepresentation(separator: String?): String {
+        return myValues.sorted().joinToString(separator.toString())
     }
 
     override fun getValue() = toStringRepresentation()
diff --git a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricPolicies.kt b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricPolicies.kt
index 4a4fde8..67d81e8 100644
--- a/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricPolicies.kt
+++ b/libraries/tools/kotlin-gradle-statistics/src/main/kotlin/org/jetbrains/kotlin/statistics/metrics/MetricPolicies.kt
@@ -5,33 +5,39 @@
 
 package org.jetbrains.kotlin.statistics.metrics
 
+import org.jetbrains.kotlin.statistics.DEFAULT_SEPARATOR
 import org.jetbrains.kotlin.statistics.ValueAnonymizer
 import org.jetbrains.kotlin.statistics.anonymizeComponentVersion
-import org.jetbrains.kotlin.statistics.sha256
 import kotlin.math.abs
 
 
-enum class StringOverridePolicy: IMetricContainerFactory<String> {
+enum class StringOverridePolicy : IMetricContainerFactory<String> {
     OVERRIDE {
-        override fun newMetricContainer(): IMetricContainer<String> = OverrideMetricContainer<String>()
+        override fun newMetricContainer(): IMetricContainer<String> = OverrideStringMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<String>? = OverrideMetricContainer(state)
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<String>? =
+            OverrideStringMetricContainer().also {
+                it.addValueFromStringPresentation(state, separator)
+            }
     },
     OVERRIDE_VERSION_IF_NOT_SET {
         override fun newMetricContainer(): IMetricContainer<String> = OverrideVersionMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<String>? = OverrideVersionMetricContainer(state)
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<String>? =
+            OverrideVersionMetricContainer(state)
     },
     CONCAT {
         override fun newMetricContainer(): IMetricContainer<String> = ConcatMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<String>? = ConcatMetricContainer(state.split(ConcatMetricContainer.SEPARATOR))
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<String>? = ConcatMetricContainer().also {
+            it.addValueFromStringPresentation(state, separator)
+        }
     }
 
     //Should be useful counting container?
 }
 
-private fun applyIfLong(v: String, action: (Long) -> IMetricContainer<Long>) : IMetricContainer<Long>? {
+private fun applyIfLong(v: String, action: (Long) -> IMetricContainer<Long>): IMetricContainer<Long>? {
     val longVal = v.toLongOrNull()
     return if (longVal == null) {
         null
@@ -40,40 +46,45 @@
     }
 }
 
-enum class NumberOverridePolicy: IMetricContainerFactory<Long> {
+enum class NumberOverridePolicy : IMetricContainerFactory<Long> {
     OVERRIDE {
-        override fun newMetricContainer(): IMetricContainer<Long> = OverrideMetricContainer<Long>()
+        override fun newMetricContainer(): IMetricContainer<Long> = OverrideLongMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<Long>? = applyIfLong(state) {
-            OverrideMetricContainer(it)
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<Long>? = applyIfLong(state) { value ->
+            OverrideLongMetricContainer().also { it.addValue(value) }
         }
     },
     SUM {
         override fun newMetricContainer(): IMetricContainer<Long> = SumMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<Long>? = applyIfLong(state) {
-            SumMetricContainer(it)
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<Long>? = SumMetricContainer().also {
+            it.addValueFromStringPresentation(state, separator)
         }
     },
     AVERAGE {
         override fun newMetricContainer(): IMetricContainer<Long> = AverageMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<Long>? = applyIfLong(state) {
-            AverageMetricContainer(it)
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<Long>? = AverageMetricContainer().also {
+            it.addValueFromStringPresentation(state, separator)
         }
+
     }
 }
 
-enum class BooleanOverridePolicy: IMetricContainerFactory<Boolean> {
+enum class BooleanOverridePolicy : IMetricContainerFactory<Boolean> {
     OVERRIDE {
-        override fun newMetricContainer(): IMetricContainer<Boolean> = OverrideMetricContainer<Boolean>()
+        override fun newMetricContainer(): IMetricContainer<Boolean> = OverrideBooleanMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<Boolean>? = OverrideMetricContainer(state.toBoolean())
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<Boolean>? =
+            OverrideBooleanMetricContainer().also {
+                it.addValueFromStringPresentation(state, separator)
+            }
     },
     OR {
         override fun newMetricContainer(): IMetricContainer<Boolean> = OrMetricContainer()
 
-        override fun fromStringRepresentation(state: String): IMetricContainer<Boolean>? = OrMetricContainer(state.toBoolean())
+        override fun fromStringRepresentation(state: String, separator: String): IMetricContainer<Boolean>? =
+            OrMetricContainer(state.toBoolean())
     }
 
     // may be add disctribution counter metric container
@@ -81,28 +92,28 @@
 
 enum class BooleanAnonymizationPolicy : ValueAnonymizer<Boolean> {
     SAFE {
-        override fun anonymize(t: Boolean) = t
+        override fun anonymize(t: Boolean, separator: String) = t
     }
 }
 
 abstract class StringAnonymizationPolicy : ValueAnonymizer<String> {
 
-    abstract fun validationRegexp(): String
+    abstract fun validationRegexp(separator: String = DEFAULT_SEPARATOR): String
 
     class AllowedListAnonymizer(val allowedValues: Collection<String>) : StringAnonymizationPolicy() {
         companion object {
             const val UNEXPECTED_VALUE = "UNEXPECTED-VALUE"
         }
 
-        override fun validationRegexp(): String {
-            return "^((${UNEXPECTED_VALUE}|${allowedValues.joinToString("|")})${ConcatMetricContainer.SEPARATOR}?)+$"
+        override fun validationRegexp(separator: String): String {
+            return "^((${UNEXPECTED_VALUE}|${allowedValues.joinToString("|")})($separator)?)+$"
         }
 
-        override fun anonymize(t: String): String {
-            return if (t.matches(Regex(validationRegexp()))) {
+        override fun anonymize(t: String, separator: String): String {
+            return if (t.matches(Regex(validationRegexp(separator)))) {
                 t
             } else {
-                t.split(ConcatMetricContainer.SEPARATOR).joinToString(ConcatMetricContainer.SEPARATOR) {
+                t.split(separator).joinToString(separator.toString()) {
                     if (allowedValues.contains(it))
                         it
                     else
@@ -113,27 +124,27 @@
     }
 
     class RegexControlled(private val regex: String, private val anonymizeInIde: Boolean) : StringAnonymizationPolicy() {
-        override fun validationRegexp() = regex
+        override fun validationRegexp(separator: String): String = regex
 
-        override fun anonymize(t: String) = t
+        override fun anonymize(t: String, separator: String) = t
 
         override fun anonymizeOnIdeSize() = anonymizeInIde
 
     }
 
     class ComponentVersionAnonymizer() : StringAnonymizationPolicy() {
-        override fun validationRegexp() = "(\\d+).(\\d+).(\\d+)-?(dev|snapshot|m\\d?|rc\\d?|beta\\d?)?"
+        override fun validationRegexp(separator: String): String = "(\\d+).(\\d+).(\\d+)-?(dev|snapshot|m\\d?|rc\\d?|beta\\d?)?"
 
-        override fun anonymize(t: String) = anonymizeComponentVersion(t)
+        override fun anonymize(t: String, separator: String) = anonymizeComponentVersion(t)
     }
 }
 
 enum class NumberAnonymizationPolicy : ValueAnonymizer<Long> {
     SAFE {
-        override fun anonymize(t: Long) = t
+        override fun anonymize(t: Long, separator: String) = t
     },
     RANDOM_10_PERCENT {
-        override fun anonymize(t: Long): Long {
+        override fun anonymize(t: Long, separator: String): Long {
             if (abs(t) < 10) return t
             val sign = if (t < 0)
                 -1
diff --git a/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/MetricPolicyTest.kt b/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/MetricPolicyTest.kt
index f9bb71a..4dcc49c 100644
--- a/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/MetricPolicyTest.kt
+++ b/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/MetricPolicyTest.kt
@@ -60,13 +60,13 @@
 
     @Test
     fun versionStringValidation() {
-        val separator = ConcatMetricContainer.SEPARATOR
-        val container = MetricsContainer()
+        val separator = "0123"
+        val container = MetricsContainer(metricValuesSeparator = separator)
         fun whenAdded(newValue: String, expected: String) {
             container.report(StringMetrics.MPP_PLATFORMS, newValue)
-            val currentValue = container.getMetric(StringMetrics.MPP_PLATFORMS)!!.toStringRepresentation()
+            val currentValue = container.getStringMetricPresentation(StringMetrics.MPP_PLATFORMS)!!
             assertEquals(expected, currentValue)
-            val regex = StringMetrics.MPP_PLATFORMS.anonymization.validationRegexp()
+            val regex = StringMetrics.MPP_PLATFORMS.anonymization.validationRegexp(separator)
             assertTrue(currentValue.matches(Regex(regex)), "'${currentValue}' should match '${regex}'")
         }
         whenAdded("js", "js")
diff --git a/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/ModuleChangesCatchingTest.kt b/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/ModuleChangesCatchingTest.kt
index 60c77e1..589edfb 100644
--- a/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/ModuleChangesCatchingTest.kt
+++ b/libraries/tools/kotlin-gradle-statistics/src/test/kotlin/org/jetbrains/kotlin/statistics/ModuleChangesCatchingTest.kt
@@ -30,7 +30,7 @@
         STRING_METRICS_EXPECTED_VERSION_AND_HASH.first +
                 BOOLEAN_METRICS_EXPECTED_VERSION_AND_HASH.first +
                 NUMERICAL_METRICS_EXPECTED_VERSION_AND_HASH.first,
-        "8b845ce616f5bdb9b885ededcdfd812d"
+        "86fb86878cd730955f7685f935bfc152"
     )
 private const val HASH_ALG = "MD5"