the total size is somewhat strange its format changes from 255 to 256 in size and interpretation
and again at 65535 / 65536   up to <MAXSTRING>


special case:

s a=$lb() zzdump a w !,$l(a)
 0000: 01                                                      .
s a=$lb("") zzdump a w !,$l(a)
 0000: 02 01
s a="" zzdump a w !,$l(a)


I had a copy of CACHE.DAT from Caché 16.2   
renamed and mounted it on IRIS
mapped the relevant globals to use it as source table

Sorry,  I had more changes. 
My approach in details

Class DC.String Extends %String  {
ClassMethod Get() As %String [ CodeMode = objectgenerator ]
{    do %code.WriteLine($c(9) _ "Quit $g(^Test.String, 0)")
    quit $$$OK  }
ClassMethod Set(%val As %String) [ CodeMode = objectgenerator ] {
    do %code.WriteLine($c(9) _ "Set ^Test.String = %val")
    do %code.WriteLine($c(9) _ "do zTest()")
    quit $$$OK }

/// this generates Method propTest()
/// but satisfies the Compiler for this class
ClassMethod Test() { quit  ;; from Data definition }

compiles this routine:

 ;(C)InterSystems, generated for class DC.String. Do NOT edit. 10/04/2019 12:00:42PM
zGet() public  Quit $g(^Test.String, 0) }
zSet(%val) public Set ^Test.String = %val
      do zTest() }
zTest() public  quit  ;; from Data definition }

and the  using class

Class DC.StringTest Extends %RegisteredObject  {
Property prop As DC.String;
Method Test()
 b   quit ;; from using Class }

compiled as 

 ;(C)InterSystems, generated for class DC.StringTest. Do NOT edit. 10/04/2019 12:02:35PM
%NormalizeObject() public {
If '$system.CLS.GetModified() Quit 1
If m%prop Set:i%prop'="" i%prop=(..propNormalize(i%prop))
Quit 1 }
%ValidateObject(force=0,checkserial=1) public {
set sc=1
If '$system.CLS.GetModified() Quit sc
If m%prop Set iv=..prop If iv'="" Set rc=(..propIsValid(iv)) If ('rc) Set sc=$$EmbedErr^%occSystem(sc,rc,5802,"prop",iv)
Quit sc }
zTest() public {
 b   quit ;; from using Class
zpropGet() public {
Quit $g(^Test.String, 0) }
zpropSet(%val) public {
Set ^Test.String = %val
do zTest() }
zpropTest() public {
 quit  ;; from Data definition

and the test

 SAMPLES>s obj=##class(DC.StringTest).%New()
SAMPLES>set obj.prop=123
 b   quit ;; from using Class

Well, the code generator can be tricky.
It's not one of my favorites. But sometimes you have no choice.

instead of 

do %code.WriteLine($c(9) _ "Quit ..Test()")


do %code.WriteLine($c(9) _ "Quit zTest()")

It worked for me best

StorageTo../..ToStorage only works in persistent classes where you move content from/to globals.
No chance without storage. 

It doesn't get called in Registered Classes and not when the object wasn't saved.
see my test with %Persistent

set obj=##class(Test.String).%New()
obj.prop    ;nothing loaded yet

set obj.prop=77 write obj.prop   ; unchanged as neither stored nor loaded
do obj.%Save(),obj,%Reload() ; force reload to trigger StorageToLogical
write obj.prop  ; and here we go

This proves that there is limited use of the approach.

Ken Olson , the founder of DEC (Digital Equipment Corp.) was  famous for its statement:

  • "I can't imagine any good reason for someone to have a computer at his home"

 Similar in the late 1950ies IBM estimated the worldwide market for computers of 15..30 systems in total.

So lack of phantasy what could be in the future is quite common also for very successful people and companies.

That's an excellent approach. But it has its limits.

  • You require the freedom to add properties to the existing class /table 
  • to feed the Global ^OBJ.DSTIME you require the object ID. That's available for  %%UPDATE, not for %%INSERT
    Any event-triggered computation occurs immediately before validation and normalization (which themselves are followed by writing the value to the database).
    so   You don't have yet the ID at insert time.
  • and there is evidently no chance for handling deletions.

so you really need to define your  triggers or have it in common for objects and SQL as in this example: 

Trigger AfterInsert [ Event = INSERT, Foreach = row/object, Time = AFTER ]
 // find id according to object or SQL
 set %ok=1 quit $$$OK

Trigger AfterUpdate [ Event = UPDATE, Foreach = row/object, Time = AFTER ]
    // find id according to object or SQL
if $d(%rowid) set id=%rowid
    else  if $isobject(pNew) set id=pNew.%Id()
  do whatever ....

 set %ok=1 quit $$$OK

Trigger AfteDelete [ Event = DELETE, Foreach = row/object, Time = AFTER ]
 // find id according to object or SQL
 set %ok=1 quit $$$OK

// find id according to object or SQL  
This highlights the fact that the ID is in different variables for object and SQL
and not all SQLTriggers have it in %rowid.
So real debugging is required.

from docs:
Class parameter  DSTIME

• parameter DSTIME;
If the DSTIME parameter is set to AUTO then the most recent filing operation in the current DSTIME value for each object will be recorded in a global, ^OBJ.DSTIME:
^OBJ.DSTIME(ExtentName,DSTIME,objectID) = filing operation
For DSTIME=AUTO the DSTIME value is recorded in ^OBJ.DSTIME and is set by the consumer of DSTIME data.
Refer to %DeepSee documentation for more information on how DSTIME is used by %DeepSee.
The filing operations are:

If the DSTIME parameter is set to MANUAL then the user is responsible for journaling object filing operations.

With MANUAL you are free to add whatever you like to your global ^OBJ.DSTIME
at the price to add your own code manually for any %Save , %Delete,  and also all SQL Triggers.

I didn't mention this. If you do it, there is no advantage to use DSTIME. you could do it with any class or global.
You just use the idea.  

you better not change   %CSP.Login but make a personal copy and change this

404 = HTTP page not found. Most likely you put your copy to the wrong directory or namespace   

And then you have to put the reference in MgmtPortal Security-> WebApplications as Login page:
Again with the correct namespace and the correct directory.